From 810be13b783d1ada77e0029635e67e7aaba59d30 Mon Sep 17 00:00:00 2001 From: Joey Dong Date: Fri, 29 Sep 2017 13:57:28 -0700 Subject: [PATCH 1/2] Add `metadata` accessors to `STPBankAccount` and `STPCard` Both of these fields are accessible from the customer when using ephemeral keys --- Stripe/NSDictionary+Stripe.h | 2 + Stripe/NSDictionary+Stripe.m | 18 ++++++++ Stripe/PublicHeaders/STPBankAccount.h | 7 +++ Stripe/PublicHeaders/STPCard.h | 7 +++ Stripe/PublicHeaders/STPSource.h | 4 +- Stripe/STPBankAccount.m | 3 ++ Stripe/STPCard.m | 3 ++ Stripe/STPSource.m | 4 +- Tests/Tests/BankAccount.json | 4 +- Tests/Tests/BitcoinSource.json | 4 +- Tests/Tests/Card.json | 4 +- Tests/Tests/NSDictionary+StripeTest.m | 61 +++++++++++++++++++++++++++ Tests/Tests/STPBankAccountTest.m | 1 + Tests/Tests/STPCardTest.m | 1 + Tests/Tests/STPSourceTest.m | 1 + 15 files changed, 118 insertions(+), 6 deletions(-) diff --git a/Stripe/NSDictionary+Stripe.h b/Stripe/NSDictionary+Stripe.h index b4d582b1a29..623f98cb64b 100644 --- a/Stripe/NSDictionary+Stripe.h +++ b/Stripe/NSDictionary+Stripe.h @@ -16,6 +16,8 @@ NS_ASSUME_NONNULL_BEGIN - (NSDictionary *)stp_dictionaryByRemovingNulls; +- (NSDictionary *)stp_dictionaryByRemovingNonStrings; + @end NS_ASSUME_NONNULL_END diff --git a/Stripe/NSDictionary+Stripe.m b/Stripe/NSDictionary+Stripe.m index 82e02917ac6..3aa24435222 100644 --- a/Stripe/NSDictionary+Stripe.m +++ b/Stripe/NSDictionary+Stripe.m @@ -10,6 +10,8 @@ #import "NSArray+Stripe.h" +NS_ASSUME_NONNULL_BEGIN + @implementation NSDictionary (Stripe) - (nullable NSDictionary *)stp_dictionaryByRemovingNullsValidatingRequiredFields:(NSArray *)requiredFields { @@ -50,6 +52,22 @@ - (NSDictionary *)stp_dictionaryByRemovingNulls { return [result copy]; } +- (NSDictionary *)stp_dictionaryByRemovingNonStrings { + NSMutableDictionary *result = [[NSMutableDictionary alloc] init]; + + [self enumerateKeysAndObjectsUsingBlock:^(id key, id obj, __unused BOOL *stop) { + if ([key isKindOfClass:[NSString class]] && [obj isKindOfClass:[NSString class]]) { + // Save valid key/value pair + result[key] = obj; + } + }]; + + // Make immutable copy + return [result copy]; +} + @end void linkNSDictionaryCategory(void){} + +NS_ASSUME_NONNULL_END diff --git a/Stripe/PublicHeaders/STPBankAccount.h b/Stripe/PublicHeaders/STPBankAccount.h index 28a43dbc9be..8b156fee56f 100644 --- a/Stripe/PublicHeaders/STPBankAccount.h +++ b/Stripe/PublicHeaders/STPBankAccount.h @@ -100,6 +100,13 @@ typedef NS_ENUM(NSInteger, STPBankAccountStatus) { */ @property (nonatomic, nullable, readonly) NSString *fingerprint; +/** + A set of key/value pairs associated with the bank account object. + + @see https://stripe.com/docs/api#metadata + */ +@property (nonatomic, copy, nullable, readonly) NSDictionary *metadata; + /** The validation status of the bank account. @see STPBankAccountStatus */ diff --git a/Stripe/PublicHeaders/STPCard.h b/Stripe/PublicHeaders/STPCard.h index e237b7263c4..93f04f15955 100644 --- a/Stripe/PublicHeaders/STPCard.h +++ b/Stripe/PublicHeaders/STPCard.h @@ -114,6 +114,13 @@ typedef NS_ENUM(NSInteger, STPCardFundingType) { */ @property (nonatomic, nullable, readonly) NSString *currency; +/** + A set of key/value pairs associated with the card object. + + @see https://stripe.com/docs/api#metadata + */ +@property (nonatomic, copy, nullable, readonly) NSDictionary *metadata; + /** Returns a string representation for the provided card brand; i.e. `[NSString stringFromBrand:STPCardBrandVisa] == @"Visa"`. diff --git a/Stripe/PublicHeaders/STPSource.h b/Stripe/PublicHeaders/STPSource.h index 2d090761689..7e362390b61 100644 --- a/Stripe/PublicHeaders/STPSource.h +++ b/Stripe/PublicHeaders/STPSource.h @@ -66,8 +66,10 @@ NS_ASSUME_NONNULL_BEGIN /** A set of key/value pairs associated with the source object. + + @see https://stripe.com/docs/api#metadata */ -@property (nonatomic, nullable, readonly) NSDictionary *metadata; +@property (nonatomic, copy, nullable, readonly) NSDictionary *metadata; /** Information about the owner of the payment instrument. diff --git a/Stripe/STPBankAccount.m b/Stripe/STPBankAccount.m index deefd4d6eba..552c90f50c7 100644 --- a/Stripe/STPBankAccount.m +++ b/Stripe/STPBankAccount.m @@ -23,6 +23,7 @@ @interface STPBankAccount () @property (nonatomic, copy, readwrite) NSString *bankName; @property (nonatomic, copy, nullable, readwrite) NSString *accountHolderName; @property (nonatomic, assign, readwrite) STPBankAccountHolderType accountHolderType; +@property (nonatomic, copy, nullable, readwrite) NSDictionary *metadata; @property (nonatomic, copy, nullable, readwrite) NSString *fingerprint; @property (nonatomic, assign, readwrite) STPBankAccountStatus status; @property (nonatomic, copy, readwrite) NSDictionary *allResponseFields; @@ -101,6 +102,7 @@ - (NSString *)description { [NSString stringWithFormat:@"country = %@", self.country], [NSString stringWithFormat:@"currency = %@", self.currency], [NSString stringWithFormat:@"fingerprint = %@", self.fingerprint], + [NSString stringWithFormat:@"metadata = %@", (self.metadata) ? @"" : nil], [NSString stringWithFormat:@"status = %@", [self.class stringFromStatus:self.status]], // Owner details @@ -144,6 +146,7 @@ + (nullable instancetype)decodedObjectFromAPIResponse:(nullable NSDictionary *)r bankAccount.country = dict[@"country"]; bankAccount.currency = dict[@"currency"]; bankAccount.fingerprint = dict[@"fingerprint"]; + bankAccount.metadata = [dict[@"metadata"] stp_dictionaryByRemovingNonStrings]; bankAccount.status = [self statusFromString:dict[@"status"]]; // Owner details diff --git a/Stripe/STPCard.m b/Stripe/STPCard.m index c7575959f83..fb47a0e1088 100644 --- a/Stripe/STPCard.m +++ b/Stripe/STPCard.m @@ -29,6 +29,7 @@ @interface STPCard () @property (nonatomic, assign, readwrite) NSUInteger expMonth; @property (nonatomic, assign, readwrite) NSUInteger expYear; @property (nonatomic, strong, readwrite) STPAddress *address; +@property (nonatomic, copy, nullable, readwrite) NSDictionary *metadata; @property (nonatomic, copy, readwrite) NSDictionary *allResponseFields; // See STPCard+Private.h @@ -152,6 +153,7 @@ - (NSString *)description { [NSString stringWithFormat:@"currency = %@", self.currency], [NSString stringWithFormat:@"dynamicLast4 = %@", self.dynamicLast4], [NSString stringWithFormat:@"isApplePayCard = %@", (self.isApplePayCard) ? @"YES" : @"NO"], + [NSString stringWithFormat:@"metadata = %@", (self.metadata) ? @"" : nil], // Cardholder details [NSString stringWithFormat:@"name = %@", (self.name) ? @"" : nil], @@ -196,6 +198,7 @@ + (nullable instancetype)decodedObjectFromAPIResponse:(nullable NSDictionary *)r card.currency = dict[@"currency"]; card.expMonth = [dict[@"exp_month"] intValue]; card.expYear = [dict[@"exp_year"] intValue]; + card.metadata = [dict[@"metadata"] stp_dictionaryByRemovingNonStrings]; card.address.name = card.name; card.address.line1 = dict[@"address_line1"]; diff --git a/Stripe/STPSource.m b/Stripe/STPSource.m index 8db302cc06f..2aa2dd85485 100644 --- a/Stripe/STPSource.m +++ b/Stripe/STPSource.m @@ -26,7 +26,7 @@ @interface STPSource () @property (nonatomic, nullable) NSString *currency; @property (nonatomic) STPSourceFlow flow; @property (nonatomic) BOOL livemode; -@property (nonatomic, nullable) NSDictionary *metadata; +@property (nonatomic, copy, nullable, readwrite) NSDictionary *metadata; @property (nonatomic, nullable) STPSourceOwner *owner; @property (nonatomic, nullable) STPSourceReceiver *receiver; @property (nonatomic, nullable) STPSourceRedirect *redirect; @@ -230,7 +230,7 @@ + (instancetype)decodedObjectFromAPIResponse:(NSDictionary *)response { source.currency = dict[@"currency"]; source.flow = [[self class] flowFromString:dict[@"flow"]]; source.livemode = [dict[@"livemode"] boolValue]; - source.metadata = dict[@"metadata"]; + source.metadata = [dict[@"metadata"] stp_dictionaryByRemovingNonStrings]; source.owner = [STPSourceOwner decodedObjectFromAPIResponse:dict[@"owner"]]; source.receiver = [STPSourceReceiver decodedObjectFromAPIResponse:dict[@"receiver"]]; source.redirect = [STPSourceRedirect decodedObjectFromAPIResponse:dict[@"redirect"]]; diff --git a/Tests/Tests/BankAccount.json b/Tests/Tests/BankAccount.json index a1daec9c829..d31a11144c9 100644 --- a/Tests/Tests/BankAccount.json +++ b/Tests/Tests/BankAccount.json @@ -11,7 +11,9 @@ "default_for_currency": false, "fingerprint": "1JWtPxqbdX5Gamtc", "last4": "6789", - "metadata": {}, + "metadata": { + "order_id": "6735" + }, "routing_number": "110000000", "status": "new" } diff --git a/Tests/Tests/BitcoinSource.json b/Tests/Tests/BitcoinSource.json index 9624f9c7f25..67c6a5aadc8 100644 --- a/Tests/Tests/BitcoinSource.json +++ b/Tests/Tests/BitcoinSource.json @@ -8,7 +8,9 @@ "currency": "usd", "flow": "receiver", "livemode": false, - "metadata": {}, + "metadata": { + "order_id": "6735" + }, "owner": { "address": { "city": "Pittsburgh", diff --git a/Tests/Tests/Card.json b/Tests/Tests/Card.json index a5baba79a8b..95edf9c0198 100644 --- a/Tests/Tests/Card.json +++ b/Tests/Tests/Card.json @@ -21,7 +21,9 @@ "fingerprint": "Xt5EWLLDS7FJjR1c", "funding": "credit", "last4": "4242", - "metadata": {}, + "metadata": { + "order_id": "6735" + }, "name": "Jane Austen", "tokenization_method": null } diff --git a/Tests/Tests/NSDictionary+StripeTest.m b/Tests/Tests/NSDictionary+StripeTest.m index df6a5097574..bcf6351d04e 100644 --- a/Tests/Tests/NSDictionary+StripeTest.m +++ b/Tests/Tests/NSDictionary+StripeTest.m @@ -16,6 +16,8 @@ @interface NSDictionary_StripeTest : XCTestCase @implementation NSDictionary_StripeTest +#pragma mark - dictionaryByRemovingNullsValidatingRequiredFields + - (void)test_dictionaryByRemovingNullsValidatingRequiredFields_removesNullsDeeply { NSDictionary *dictionary = @{ @"id": @"card_123", @@ -102,5 +104,64 @@ - (void)test_dictionaryByRemovingNullsValidatingRequiredFields_missingRequiredFi XCTAssertNil([dictionary stp_dictionaryByRemovingNullsValidatingRequiredFields:requiredFields]); } +#pragma mark - dictionaryByRemovingNonStrings + +- (void)test_dictionaryByRemovingNonStrings_basicCases { + NSDictionary *dictionary; + NSDictionary *expected; + NSDictionary *result; + + // Empty dictionary + dictionary = @{}; + expected = @{}; + result = [dictionary stp_dictionaryByRemovingNonStrings]; + XCTAssertEqualObjects(result, expected); + + // Regular case + dictionary = @{ + @"user": @"user_123", + @"nicknames": @"John, Johnny", + }; + expected = @{ + @"user": @"user_123", + @"nicknames": @"John, Johnny", + }; + result = [dictionary stp_dictionaryByRemovingNonStrings]; + XCTAssertEqualObjects(result, expected); + + // Strips non-NSString keys and values + dictionary = @{ + @"user": @"user_123", + @"nicknames": @"John, Johnny", + @"profiles": [NSNull null], + [NSNull null]: @"San Francisco, CA", + [NSNull null]: [NSNull null], + @"age": @(21), + @(21): @"age", + @(21): @(21), + @"fees": @{ + @"plan": @"monthly", + }, + @"visits": @[ + @"january", + @"february", + ], + }; + expected = @{ + @"user": @"user_123", + @"nicknames": @"John, Johnny", + }; + result = [dictionary stp_dictionaryByRemovingNonStrings]; + XCTAssertEqualObjects(result, expected); +} + +- (void)test_dictionaryByRemovingNonStrings_returnsImmutableCopy { + NSDictionary *dictionary = @{@"user": @"user_123"}; + NSDictionary *result = [dictionary stp_dictionaryByRemovingNonStrings]; + + XCTAssert(result); + XCTAssertNotEqual(result, dictionary); + XCTAssertFalse([result isKindOfClass:[NSMutableDictionary class]]); +} @end diff --git a/Tests/Tests/STPBankAccountTest.m b/Tests/Tests/STPBankAccountTest.m index bbb8bffbc6d..8fadfbaed19 100644 --- a/Tests/Tests/STPBankAccountTest.m +++ b/Tests/Tests/STPBankAccountTest.m @@ -139,6 +139,7 @@ - (void)testDecodedObjectFromAPIResponseMapping { XCTAssertEqualObjects(bankAccount.currency, @"usd"); XCTAssertEqualObjects(bankAccount.fingerprint, @"1JWtPxqbdX5Gamtc"); XCTAssertEqualObjects(bankAccount.last4, @"6789"); + XCTAssertEqualObjects(bankAccount.metadata, @{@"order_id": @"6735"}); XCTAssertEqualObjects(bankAccount.routingNumber, @"110000000"); XCTAssertEqual(bankAccount.status, STPBankAccountStatusNew); diff --git a/Tests/Tests/STPCardTest.m b/Tests/Tests/STPCardTest.m index c0e0d5aa14a..ff4c4cda8cb 100644 --- a/Tests/Tests/STPCardTest.m +++ b/Tests/Tests/STPCardTest.m @@ -283,6 +283,7 @@ - (void)testDecodedObjectFromAPIResponseMapping { XCTAssertEqual(card.expYear, (NSUInteger)2017); XCTAssertEqual(card.funding, STPCardFundingTypeCredit); XCTAssertEqualObjects(card.last4, @"4242"); + XCTAssertEqualObjects(card.metadata, @{@"order_id": @"6735"}); XCTAssertEqualObjects(card.name, @"Jane Austen"); XCTAssertNotEqual(card.allResponseFields, response); diff --git a/Tests/Tests/STPSourceTest.m b/Tests/Tests/STPSourceTest.m index 19d11d50b28..540ee514c4f 100644 --- a/Tests/Tests/STPSourceTest.m +++ b/Tests/Tests/STPSourceTest.m @@ -329,6 +329,7 @@ - (void)testDecodedObjectFromAPIResponseMapping { XCTAssertEqualObjects(source.currency, @"usd"); XCTAssertEqual(source.flow, STPSourceFlowReceiver); XCTAssertFalse(source.livemode); + XCTAssertEqualObjects(source.metadata, @{@"order_id": @"6735"}); XCTAssert(source.owner); // STPSourceOwnerTest XCTAssert(source.receiver); // STPSourceReceiverTest XCTAssertEqual(source.status, STPSourceStatusPending); From 1a59452da101cd0c2b0fd1beb2c239bf52e7dc3e Mon Sep 17 00:00:00 2001 From: Joey Dong Date: Fri, 29 Sep 2017 14:01:56 -0700 Subject: [PATCH 2/2] Add CHANGELOG entry for new `metadata` accessors --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 088e005c969..0e45831f613 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 11.x.x 2017-XX-XX * Restores `[STPCard brandFromString:]` method which was marked as deprecated in a recent version [#801](https://github.com/stripe/stripe-ios/pull/801) +* Adds `[STPBankAccount metadata]` and `[STPCard metadata]` read-only accessors and improves annotation for `[STPSource metadata]` [#808](https://github.com/stripe/stripe-ios/pull/808) ## 11.3.0 2017-09-13 * Adds support for creating `STPSourceParams` for P24 source [#779](https://github.com/stripe/stripe-ios/pull/779)