Skip to content

Commit 2ff755e

Browse files
committed
Merge pull request #304 from stripe/jack-additional-params
Add additionalFields to STPFormEncodable/STPAPIResponseDecodable
2 parents 59eec38 + de6de29 commit 2ff755e

15 files changed

+372
-135
lines changed

Stripe/PublicHeaders/STPAPIResponseDecodable.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,19 @@
1010

1111
@protocol STPAPIResponseDecodable <NSObject>
1212

13+
/**
14+
* These fields are required to be present in the API response. If any of them are nil, decodedObjectFromAPIResponse should also return nil.
15+
*/
1316
+ (nonnull NSArray *)requiredFields;
17+
18+
/**
19+
* Parses an response from the Stripe API (in JSON format; represented as an NSDictionary) into an instance of the class. Returns nil if the object could not be decoded (i.e. if one of its `requiredFields` is nil).
20+
*/
1421
+ (nullable instancetype)decodedObjectFromAPIResponse:(nonnull NSDictionary *)response;
1522

23+
/**
24+
* The raw JSON response used to create the object. This can be useful for using beta features that haven't yet been made into properties in the SDK.
25+
*/
26+
@property(nonatomic, readonly, nonnull, copy)NSDictionary *allResponseFields;
27+
1628
@end

Stripe/PublicHeaders/STPFormEncodable.h

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,28 @@
88

99
#import <Foundation/Foundation.h>
1010

11+
/**
12+
* Objects conforming to STPFormEncodable can be automatically converted to a form-encoded string, which can then be used when making requests to the Stripe API.
13+
*/
1114
@protocol STPFormEncodable <NSObject>
1215

13-
+ (NSString *)rootObjectName;
14-
+ (NSDictionary *)propertyNamesToFormFieldNamesMapping;
16+
/**
17+
* The root object name to be used when converting this object to a form-encoded string. For example, if this returns @"card", then the form-encoded output will resemble @"card[foo]=bar" (where 'foo' and 'bar' are specified by `propertyNamesToFormFieldNamesMapping` below.
18+
*/
19+
+ (nonnull NSString *)rootObjectName;
20+
21+
/**
22+
* This maps properties on an object that is being form-encoded into parameter names in the Stripe API. For example, STPCardParams has a field called `expMonth`, but the Stripe API expects a field called `exp_month`. This dictionary represents a mapping from the former to the latter (in other words, [STPCardParams propertyNamesToFormFieldNamesMapping][@"expMonth"] == @"exp_month".)
23+
*/
24+
+ (nonnull NSDictionary *)propertyNamesToFormFieldNamesMapping;
25+
26+
/**
27+
* You can use this property to add additional fields to an API request that are not explicitly defined by the object's interface. This can be useful when using beta features that haven't been added to the Stripe SDK yet. For example, if the /v1/tokens API began to accept a beta field called "test_field", you might do the following:
28+
STPCardParams *cardParams = [STPCardParams new];
29+
// add card values
30+
cardParams.additionalAPIParameters = @{@"test_field": @"example_value"};
31+
[[STPAPIClient sharedClient] createTokenWithCard:cardParams completion:...];
32+
*/
33+
@property(nonatomic, readwrite, nonnull, copy)NSDictionary *additionalAPIParameters;
1534

1635
@end

Stripe/STPBankAccount.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ @interface STPBankAccount ()
1616
@property (nonatomic, readwrite) NSString *bankName;
1717
@property (nonatomic, readwrite) NSString *fingerprint;
1818
@property (nonatomic) STPBankAccountStatus status;
19+
@property (nonatomic, readwrite, nonnull, copy) NSDictionary *allResponseFields;
1920

2021
@end
2122

@@ -90,6 +91,8 @@ + (instancetype)decodedObjectFromAPIResponse:(NSDictionary *)response {
9091
} else if ([status isEqual: @"errored"]) {
9192
bankAccount.status = STPBankAccountStatusErrored;
9293
}
94+
95+
bankAccount.allResponseFields = dict;
9396
return bankAccount;
9497
}
9598

Stripe/STPBankAccountParams.m

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@
1010

1111
@implementation STPBankAccountParams
1212

13+
@synthesize additionalAPIParameters = _additionalAPIParameters;
14+
15+
- (instancetype)init {
16+
self = [super init];
17+
if (self) {
18+
_additionalAPIParameters = @{};
19+
}
20+
return self;
21+
}
22+
1323
- (NSString *)last4 {
1424
if (self.accountNumber && self.accountNumber.length >= 4) {
1525
return [self.accountNumber substringFromIndex:(self.accountNumber.length - 4)];

Stripe/STPCard.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ @interface STPCard ()
2020
@property (nonatomic, readwrite) STPCardFundingType funding;
2121
@property (nonatomic, readwrite) NSString *fingerprint;
2222
@property (nonatomic, readwrite) NSString *country;
23+
@property (nonatomic, readwrite, nonnull, copy) NSDictionary *allResponseFields;
2324

2425
@end
2526

@@ -135,6 +136,8 @@ + (instancetype)decodedObjectFromAPIResponse:(NSDictionary *)response {
135136
card.addressState = dict[@"address_state"];
136137
card.addressZip = dict[@"address_zip"];
137138
card.addressCountry = dict[@"address_country"];
139+
140+
card.allResponseFields = dict;
138141
return card;
139142
}
140143
#pragma clang diagnostic pop

Stripe/STPCardParams.m

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@
1212

1313
@implementation STPCardParams
1414

15+
@synthesize additionalAPIParameters = _additionalAPIParameters;
16+
17+
- (instancetype)init {
18+
self = [super init];
19+
if (self) {
20+
_additionalAPIParameters = @{};
21+
}
22+
return self;
23+
}
24+
1525
- (NSString *)last4 {
1626
if (self.number && self.number.length >= 4) {
1727
return [self.number substringFromIndex:(self.number.length - 4)];

Stripe/STPFormEncoder.m

Lines changed: 157 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,49 +11,176 @@
1111
#import "STPCardParams.h"
1212
#import "STPFormEncodable.h"
1313

14+
FOUNDATION_EXPORT NSString * STPPercentEscapedStringFromString(NSString *string);
15+
FOUNDATION_EXPORT NSString * STPQueryStringFromParameters(NSDictionary *parameters);
16+
1417
@implementation STPFormEncoder
1518

19+
+ (NSString *)stringByReplacingSnakeCaseWithCamelCase:(NSString *)input {
20+
NSArray *parts = [input componentsSeparatedByString:@"_"];
21+
NSMutableString *camelCaseParam = [NSMutableString string];
22+
[parts enumerateObjectsUsingBlock:^(NSString *part, NSUInteger idx, __unused BOOL *stop) {
23+
[camelCaseParam appendString:(idx == 0 ? part : [part capitalizedString])];
24+
}];
25+
26+
return [camelCaseParam copy];
27+
}
28+
1629
+ (nonnull NSData *)formEncodedDataForObject:(nonnull NSObject<STPFormEncodable> *)object {
17-
NSMutableArray *parts = [NSMutableArray array];
30+
NSDictionary *dict = @{
31+
[object.class rootObjectName]: [self keyPairDictionaryForObject:object]
32+
};
33+
return [STPQueryStringFromParameters(dict) dataUsingEncoding:NSUTF8StringEncoding];
34+
}
35+
36+
+ (NSDictionary *)keyPairDictionaryForObject:(nonnull NSObject<STPFormEncodable> *)object {
37+
NSMutableDictionary *keyPairs = [NSMutableDictionary dictionary];
1838
[[object.class propertyNamesToFormFieldNamesMapping] enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull propertyName, NSString * _Nonnull formFieldName, __unused BOOL * _Nonnull stop) {
19-
NSString *formFieldValue = [[object valueForKey:propertyName] description];
20-
if (formFieldValue) {
21-
[parts addObject:[NSString stringWithFormat:@"%@[%@]=%@", [object.class rootObjectName], formFieldName, [self.class stringByURLEncoding:formFieldValue]]];
39+
id value = [self formEncodableValueForObject:[object valueForKey:propertyName]];
40+
if (value) {
41+
keyPairs[formFieldName] = value;
42+
}
43+
}];
44+
[object.additionalAPIParameters enumerateKeysAndObjectsUsingBlock:^(id _Nonnull additionalFieldName, id _Nonnull additionalFieldValue, __unused BOOL * _Nonnull stop) {
45+
id value = [self formEncodableValueForObject:additionalFieldValue];
46+
if (value) {
47+
keyPairs[additionalFieldName] = value;
2248
}
2349
}];
24-
return [[parts componentsJoinedByString:@"&"] dataUsingEncoding:NSUTF8StringEncoding];
50+
return [keyPairs copy];
51+
}
52+
53+
+ (id)formEncodableValueForObject:(NSObject *)object {
54+
if ([object conformsToProtocol:@protocol(STPFormEncodable)]) {
55+
return [self keyPairDictionaryForObject:(NSObject<STPFormEncodable>*)object];
56+
} else {
57+
return object;
58+
}
2559
}
2660

27-
/* This code is adapted from the code by David DeLong in this StackOverflow post:
28-
http://stackoverflow.com/questions/3423545/objective-c-iphone-percent-encode-a-string . It is protected under the terms of a Creative Commons
29-
license: http://creativecommons.org/licenses/by-sa/3.0/
30-
*/
3161
+ (NSString *)stringByURLEncoding:(NSString *)string {
32-
NSMutableString *output = [NSMutableString string];
33-
const unsigned char *source = (const unsigned char *)[string UTF8String];
34-
NSInteger sourceLen = strlen((const char *)source);
35-
for (int i = 0; i < sourceLen; ++i) {
36-
const unsigned char thisChar = source[i];
37-
if (thisChar == ' ') {
38-
[output appendString:@"+"];
39-
} else if (thisChar == '.' || thisChar == '-' || thisChar == '_' || thisChar == '~' || (thisChar >= 'a' && thisChar <= 'z') ||
40-
(thisChar >= 'A' && thisChar <= 'Z') || (thisChar >= '0' && thisChar <= '9')) {
41-
[output appendFormat:@"%c", thisChar];
42-
} else {
43-
[output appendFormat:@"%%%02X", thisChar];
44-
}
62+
return STPPercentEscapedStringFromString(string);
63+
}
64+
65+
@end
66+
67+
68+
// This code is adapted from https://github.com/AFNetworking/AFNetworking/blob/master/AFNetworking/AFURLRequestSerialization.m . The only modifications are to replace the AF namespace with the STP namespace to avoid collisions with apps that are using both Stripe and AFNetworking.
69+
NSString * STPPercentEscapedStringFromString(NSString *string) {
70+
static NSString * const kSTPCharactersGeneralDelimitersToEncode = @":#[]@"; // does not include "?" or "/" due to RFC 3986 - Section 3.4
71+
static NSString * const kSTPCharactersSubDelimitersToEncode = @"!$&'()*+,;=";
72+
73+
NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
74+
[allowedCharacterSet removeCharactersInString:[kSTPCharactersGeneralDelimitersToEncode stringByAppendingString:kSTPCharactersSubDelimitersToEncode]];
75+
76+
// FIXME: https://github.com/AFNetworking/AFNetworking/pull/3028
77+
// return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
78+
79+
static NSUInteger const batchSize = 50;
80+
81+
NSUInteger index = 0;
82+
NSMutableString *escaped = @"".mutableCopy;
83+
84+
while (index < string.length) {
85+
#pragma GCC diagnostic push
86+
#pragma GCC diagnostic ignored "-Wgnu"
87+
NSUInteger length = MIN(string.length - index, batchSize);
88+
#pragma GCC diagnostic pop
89+
NSRange range = NSMakeRange(index, length);
90+
91+
// To avoid breaking up character sequences such as 👴🏻👮🏽
92+
range = [string rangeOfComposedCharacterSequencesForRange:range];
93+
94+
NSString *substring = [string substringWithRange:range];
95+
NSString *encoded = [substring stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
96+
[escaped appendString:encoded];
97+
98+
index += range.length;
4599
}
46-
return output;
100+
101+
return escaped;
47102
}
48103

49-
+ (NSString *)stringByReplacingSnakeCaseWithCamelCase:(NSString *)input {
50-
NSArray *parts = [input componentsSeparatedByString:@"_"];
51-
NSMutableString *camelCaseParam = [NSMutableString string];
52-
[parts enumerateObjectsUsingBlock:^(NSString *part, NSUInteger idx, __unused BOOL *stop) {
53-
[camelCaseParam appendString:(idx == 0 ? part : [part capitalizedString])];
54-
}];
104+
#pragma mark -
105+
106+
@interface STPQueryStringPair : NSObject
107+
@property (readwrite, nonatomic, strong) id field;
108+
@property (readwrite, nonatomic, strong) id value;
109+
110+
- (instancetype)initWithField:(id)field value:(id)value;
111+
112+
- (NSString *)URLEncodedStringValue;
113+
@end
114+
115+
@implementation STPQueryStringPair
116+
117+
- (instancetype)initWithField:(id)field value:(id)value {
118+
self = [super init];
119+
if (!self) {
120+
return nil;
121+
}
55122

56-
return [camelCaseParam copy];
123+
_field = field;
124+
_value = value;
125+
126+
return self;
127+
}
128+
129+
- (NSString *)URLEncodedStringValue {
130+
if (!self.value || [self.value isEqual:[NSNull null]]) {
131+
return STPPercentEscapedStringFromString([self.field description]);
132+
} else {
133+
return [NSString stringWithFormat:@"%@=%@", STPPercentEscapedStringFromString([self.field description]), STPPercentEscapedStringFromString([self.value description])];
134+
}
57135
}
58136

59137
@end
138+
139+
#pragma mark -
140+
141+
FOUNDATION_EXPORT NSArray * STPQueryStringPairsFromDictionary(NSDictionary *dictionary);
142+
FOUNDATION_EXPORT NSArray * STPQueryStringPairsFromKeyAndValue(NSString *key, id value);
143+
144+
NSString * STPQueryStringFromParameters(NSDictionary *parameters) {
145+
NSMutableArray *mutablePairs = [NSMutableArray array];
146+
for (STPQueryStringPair *pair in STPQueryStringPairsFromDictionary(parameters)) {
147+
[mutablePairs addObject:[pair URLEncodedStringValue]];
148+
}
149+
150+
return [mutablePairs componentsJoinedByString:@"&"];
151+
}
152+
153+
NSArray * STPQueryStringPairsFromDictionary(NSDictionary *dictionary) {
154+
return STPQueryStringPairsFromKeyAndValue(nil, dictionary);
155+
}
156+
157+
NSArray * STPQueryStringPairsFromKeyAndValue(NSString *key, id value) {
158+
NSMutableArray *mutableQueryStringComponents = [NSMutableArray array];
159+
NSString *descriptionSelector = NSStringFromSelector(@selector(description));
160+
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:descriptionSelector ascending:YES selector:@selector(compare:)];
161+
162+
if ([value isKindOfClass:[NSDictionary class]]) {
163+
NSDictionary *dictionary = value;
164+
// Sort dictionary keys to ensure consistent ordering in query string, which is important when deserializing potentially ambiguous sequences, such as an array of dictionaries
165+
for (id nestedKey in [dictionary.allKeys sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
166+
id nestedValue = dictionary[nestedKey];
167+
if (nestedValue) {
168+
[mutableQueryStringComponents addObjectsFromArray:STPQueryStringPairsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)];
169+
}
170+
}
171+
} else if ([value isKindOfClass:[NSArray class]]) {
172+
NSArray *array = value;
173+
for (id nestedValue in array) {
174+
[mutableQueryStringComponents addObjectsFromArray:STPQueryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)];
175+
}
176+
} else if ([value isKindOfClass:[NSSet class]]) {
177+
NSSet *set = value;
178+
for (id obj in [set sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
179+
[mutableQueryStringComponents addObjectsFromArray:STPQueryStringPairsFromKeyAndValue(key, obj)];
180+
}
181+
} else {
182+
[mutableQueryStringComponents addObject:[[STPQueryStringPair alloc] initWithField:key value:value]];
183+
}
184+
185+
return mutableQueryStringComponents;
186+
}

Stripe/STPToken.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ @interface STPToken()
1717
@property (nonatomic, nullable) STPCard *card;
1818
@property (nonatomic, nullable) STPBankAccount *bankAccount;
1919
@property (nonatomic, nullable) NSDate *created;
20+
@property (nonatomic, readwrite, nonnull, copy) NSDictionary *allResponseFields;
2021
@end
2122

2223
@implementation STPToken
@@ -86,6 +87,8 @@ + (instancetype)decodedObjectFromAPIResponse:(NSDictionary *)response {
8687
if (bankAccountDictionary) {
8788
token.bankAccount = [STPBankAccount decodedObjectFromAPIResponse:bankAccountDictionary];
8889
}
90+
91+
token.allResponseFields = dict;
8992
return token;
9093
}
9194

Tests/Tests/STPApplePayTest.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ - (void)testCreateTokenWithPayment {
6565

6666
STPAPIClient *client = [[STPAPIClient alloc] initWithPublishableKey:@"pk_test_vOo1umqsYxSrP5UXfOeL3ecm"];
6767

68-
XCTestExpectation *expectation = [self expectationWithDescription:@"Bank account creation"];
68+
XCTestExpectation *expectation = [self expectationWithDescription:@"Apple pay token creation"];
6969
[client createTokenWithPayment:payment
7070
completion:^(STPToken *token, NSError *error) {
7171
[expectation fulfill];

Tests/Tests/STPBankAccountFunctionalTest.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ - (void)testCreateAndRetreiveBankAccountToken {
2424
bankAccount.routingNumber = @"110000000";
2525
bankAccount.country = @"US";
2626

27-
STPAPIClient *client = [[STPAPIClient alloc] initWithPublishableKey:@"pk_test_5fhKkYDKKNr4Fp6q7Mq9CwJd"];
27+
STPAPIClient *client = [[STPAPIClient alloc] initWithPublishableKey:@"pk_test_vOo1umqsYxSrP5UXfOeL3ecm"];
2828

2929
XCTestExpectation *expectation = [self expectationWithDescription:@"Bank account creation"];
3030
[client createTokenWithBankAccount:bankAccount

0 commit comments

Comments
 (0)