From a522953fb487fd417c5f2752d852d7372f7d681b Mon Sep 17 00:00:00 2001
From: Dan Jackson <danj@stripe.com>
Date: Wed, 11 Jul 2018 16:26:43 -0700
Subject: [PATCH 1/3] Fix completion block type in `STPCustomerContextTest`

This did not have a functional impact on the testing, but it sure was misleading
while reading through the code.
---
 Tests/Tests/STPCustomerContextTest.m | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Tests/Tests/STPCustomerContextTest.m b/Tests/Tests/STPCustomerContextTest.m
index 19ed3c95e02..77c1a938d01 100644
--- a/Tests/Tests/STPCustomerContextTest.m
+++ b/Tests/Tests/STPCustomerContextTest.m
@@ -285,9 +285,9 @@ - (void)testDetachSourceFromCustomerCallsAPIClientCorrectly {
                    fromCustomerUsingKey:[OCMArg isEqual:customerKey]
                              completion:[OCMArg any]])
     .andDo(^(NSInvocation *invocation) {
-        STPCustomerCompletionBlock completion;
+        STPSourceProtocolCompletionBlock completion;
         [invocation getArgument:&completion atIndex:4];
-        completion([STPFixtures customerWithSingleCardTokenSource], nil);
+        completion([STPFixtures cardSource], nil);
         [exp fulfill];
     });
     id mockKeyManager = [self mockKeyManagerWithKey:customerKey];

From 02ef56f124c586cbac61c6820fe552f56f93d8d5 Mon Sep 17 00:00:00 2001
From: Dan Jackson <danj@stripe.com>
Date: Wed, 11 Jul 2018 17:28:22 -0700
Subject: [PATCH 2/3] Add STPGenericStripeObject to represent a minimal,
 generic object.

Some of the Stripe APIs, notably https://stripe.com/docs/api#delete_card return an
almost empty object. The parsing code needs to be able to parse an object out of the
response, otherwise it considers it failed.

This is an object that should be parseable from any successful response. It's pretty
useless otherwise.
---
 Stripe.xcodeproj/project.pbxproj         | 16 ++++++++++
 Stripe/STPGenericStripeObject.h          | 37 ++++++++++++++++++++++++
 Stripe/STPGenericStripeObject.m          | 36 +++++++++++++++++++++++
 Tests/Tests/STPGenericStripeObjectTest.m | 27 +++++++++++++++++
 4 files changed, 116 insertions(+)
 create mode 100644 Stripe/STPGenericStripeObject.h
 create mode 100644 Stripe/STPGenericStripeObject.m
 create mode 100644 Tests/Tests/STPGenericStripeObjectTest.m

diff --git a/Stripe.xcodeproj/project.pbxproj b/Stripe.xcodeproj/project.pbxproj
index 984ce409512..491afa91d0f 100644
--- a/Stripe.xcodeproj/project.pbxproj
+++ b/Stripe.xcodeproj/project.pbxproj
@@ -373,6 +373,11 @@
 		8BD87B951EFB1CB100269C2B /* STPSourceVerificationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BD87B941EFB1CB100269C2B /* STPSourceVerificationTest.m */; };
 		8BE5AE8B1EF8905B0081A33C /* STPCardParamsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BE5AE8A1EF8905B0081A33C /* STPCardParamsTest.m */; };
 		B318518320BE011700EE8C0F /* STPColorUtilsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B318518220BE011700EE8C0F /* STPColorUtilsTest.m */; };
+		B32B175E20F6D2C4000D6EF8 /* STPGenericStripeObject.h in Headers */ = {isa = PBXBuildFile; fileRef = B32B175C20F6D2C4000D6EF8 /* STPGenericStripeObject.h */; };
+		B32B175F20F6D2C4000D6EF8 /* STPGenericStripeObject.h in Headers */ = {isa = PBXBuildFile; fileRef = B32B175C20F6D2C4000D6EF8 /* STPGenericStripeObject.h */; };
+		B32B176020F6D2C4000D6EF8 /* STPGenericStripeObject.m in Sources */ = {isa = PBXBuildFile; fileRef = B32B175D20F6D2C4000D6EF8 /* STPGenericStripeObject.m */; };
+		B32B176120F6D2C4000D6EF8 /* STPGenericStripeObject.m in Sources */ = {isa = PBXBuildFile; fileRef = B32B175D20F6D2C4000D6EF8 /* STPGenericStripeObject.m */; };
+		B32B176320F6D722000D6EF8 /* STPGenericStripeObjectTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B32B176220F6D722000D6EF8 /* STPGenericStripeObjectTest.m */; };
 		B3302F462006FBA7005DDBE9 /* STPConnectAccountParamsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B3302F452006FBA7005DDBE9 /* STPConnectAccountParamsTest.m */; };
 		B3302F4C200700AB005DDBE9 /* STPLegalEntityParamsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B3302F4B200700AB005DDBE9 /* STPLegalEntityParamsTest.m */; };
 		B347DD481FE35423006B3BAC /* STPValidatedTextField.h in Headers */ = {isa = PBXBuildFile; fileRef = B347DD461FE35423006B3BAC /* STPValidatedTextField.h */; };
@@ -1063,6 +1068,9 @@
 		8BD87B941EFB1CB100269C2B /* STPSourceVerificationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPSourceVerificationTest.m; sourceTree = "<group>"; };
 		8BE5AE8A1EF8905B0081A33C /* STPCardParamsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPCardParamsTest.m; sourceTree = "<group>"; };
 		B318518220BE011700EE8C0F /* STPColorUtilsTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPColorUtilsTest.m; sourceTree = "<group>"; };
+		B32B175C20F6D2C4000D6EF8 /* STPGenericStripeObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STPGenericStripeObject.h; sourceTree = "<group>"; };
+		B32B175D20F6D2C4000D6EF8 /* STPGenericStripeObject.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPGenericStripeObject.m; sourceTree = "<group>"; };
+		B32B176220F6D722000D6EF8 /* STPGenericStripeObjectTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPGenericStripeObjectTest.m; sourceTree = "<group>"; };
 		B3302F452006FBA7005DDBE9 /* STPConnectAccountParamsTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPConnectAccountParamsTest.m; sourceTree = "<group>"; };
 		B3302F4B200700AB005DDBE9 /* STPLegalEntityParamsTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPLegalEntityParamsTest.m; sourceTree = "<group>"; };
 		B347DD461FE35423006B3BAC /* STPValidatedTextField.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STPValidatedTextField.h; sourceTree = "<group>"; };
@@ -1645,6 +1653,7 @@
 				C1CFCB701ED5E11500BE45DF /* STPFileTest.m */,
 				04CDB51F1A5F3A9300B854EE /* STPFormEncoderTest.m */,
 				C16F66AA1CA21BAC006A21B5 /* STPFormTextFieldTest.m */,
+				B32B176220F6D722000D6EF8 /* STPGenericStripeObjectTest.m */,
 				04827D171D257A6C002DB3E8 /* STPImageLibraryTest.m */,
 				B3302F4B200700AB005DDBE9 /* STPLegalEntityParamsTest.m */,
 				045A62AA1B8E7259000165CE /* STPPaymentCardTextFieldTest.m */,
@@ -1900,6 +1909,8 @@
 				8B429ADD1EF9EFF600F95F34 /* STPFile+Private.h */,
 				04CDB4C41A5F30A700B854EE /* STPFormEncoder.h */,
 				04CDB4C51A5F30A700B854EE /* STPFormEncoder.m */,
+				B32B175C20F6D2C4000D6EF8 /* STPGenericStripeObject.h */,
+				B32B175D20F6D2C4000D6EF8 /* STPGenericStripeObject.m */,
 				C1CFCB661ED4E38900BE45DF /* STPInternalAPIResponseDecodable.h */,
 				F1D3A2471EB012010095BFA9 /* STPMultipartFormDataEncoder.h */,
 				F1D3A2481EB012010095BFA9 /* STPMultipartFormDataEncoder.m */,
@@ -2187,6 +2198,7 @@
 				0438EF491B74183100D506CC /* STPCardBrand.h in Headers */,
 				F12C8DC21D63DE9F00ADA0D7 /* STPPaymentContextAmountModel.h in Headers */,
 				F1D96F971DC7D82400477E64 /* STPLocalizationUtils.h in Headers */,
+				B32B175F20F6D2C4000D6EF8 /* STPGenericStripeObject.h in Headers */,
 				049A3FA91CC96B3B00F57DE7 /* STPBackendAPIAdapter.h in Headers */,
 				049952D61BCF14930088C703 /* STPAPIRequest.h in Headers */,
 				04827D111D2575C6002DB3E8 /* STPImageLibrary.h in Headers */,
@@ -2330,6 +2342,7 @@
 				F12829DA1D7747E4008B10D6 /* STPBundleLocator.h in Headers */,
 				C11810951CC6C4700022FB55 /* PKPaymentAuthorizationViewController+Stripe_Blocks.h in Headers */,
 				04CDB5161A5F30A700B854EE /* StripeError.h in Headers */,
+				B32B175E20F6D2C4000D6EF8 /* STPGenericStripeObject.h in Headers */,
 				049A3F991CC76A2400F57DE7 /* NSBundle+Stripe_AppName.h in Headers */,
 				04E32A9D1B7A9490009C9E35 /* STPPaymentCardTextField.h in Headers */,
 				C1BD9B221E393FFE00CEE925 /* STPSourceReceiver.h in Headers */,
@@ -2843,6 +2856,7 @@
 				C11810991CC6D46D0022FB55 /* NSDecimalNumber+StripeTest.m in Sources */,
 				8B5B4B441EFDD925005CF475 /* STPSourceOwnerTest.m in Sources */,
 				8B82C5CA1F2BC78F009639F7 /* STPApplePayPaymentMethodTest.m in Sources */,
+				B32B176320F6D722000D6EF8 /* STPGenericStripeObjectTest.m in Sources */,
 				B3BDCACF20EEF4640034F7F5 /* STPPaymentIntentFunctionalTest.m in Sources */,
 				8B013C891F1E784A00DD831B /* STPPaymentConfigurationTest.m in Sources */,
 				C1EEDCC81CA2172700A54582 /* NSString+StripeTest.m in Sources */,
@@ -2935,6 +2949,7 @@
 				B3A2413C1FFEB57400A2F00D /* STPConnectAccountParams.m in Sources */,
 				C1BD9B2B1E39406C00CEE925 /* STPSourceOwner.m in Sources */,
 				C1BD9B311E3940A200CEE925 /* STPSourceRedirect.m in Sources */,
+				B32B176120F6D2C4000D6EF8 /* STPGenericStripeObject.m in Sources */,
 				04B31DE91D09D25F00EF1631 /* STPPaymentMethodsInternalViewController.m in Sources */,
 				F12C8DC51D63DE9F00ADA0D7 /* STPPaymentContextAmountModel.m in Sources */,
 				04633B011CD129CB009D4FB5 /* STPPhoneNumberValidator.m in Sources */,
@@ -3001,6 +3016,7 @@
 				0438EF2F1B7416BB00D506CC /* STPFormTextField.m in Sources */,
 				045D71101CEEE30500F6CD65 /* STPAspects.m in Sources */,
 				F152321D1EA92FC100D65C67 /* STPRedirectContext.m in Sources */,
+				B32B176020F6D2C4000D6EF8 /* STPGenericStripeObject.m in Sources */,
 				04B31E011D131D9000EF1631 /* STPPaymentCardTextFieldCell.m in Sources */,
 				04633B0D1CD44F6C009D4FB5 /* PKPayment+Stripe.m in Sources */,
 				F1852F951D80B6EC00367C86 /* STPStringUtils.m in Sources */,
diff --git a/Stripe/STPGenericStripeObject.h b/Stripe/STPGenericStripeObject.h
new file mode 100644
index 00000000000..b5f623b62a7
--- /dev/null
+++ b/Stripe/STPGenericStripeObject.h
@@ -0,0 +1,37 @@
+//
+//  STPGenericStripeObject.h
+//  Stripe
+//
+//  Created by Daniel Jackson on 7/11/18.
+//  Copyright © 2018 Stripe, Inc. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+#import "STPAPIResponseDecodable.h"
+#import "STPSourceProtocol.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ Generic decodable Stripe object. It only has an `id`
+
+ `STPAPIRequest` expects to be able to parse an object out of the result, otherwise
+ it considers the request to have failed.
+ This primarily exists to handle the response to calls like these:
+ - https://stripe.com/docs/api#delete_card + https://stripe.com/docs/api#detach_source
+ - https://stripe.com/docs/api#customer_delete_bank_account
+
+ This will probably never be useful to expose publicly, the caller probably already has the
+ id.
+ */
+@interface STPGenericStripeObject : NSObject <STPAPIResponseDecodable>
+
+/**
+ The stripe id of this object.
+ */
+@property (nonatomic, readonly) NSString *stripeId;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Stripe/STPGenericStripeObject.m b/Stripe/STPGenericStripeObject.m
new file mode 100644
index 00000000000..7da759802ae
--- /dev/null
+++ b/Stripe/STPGenericStripeObject.m
@@ -0,0 +1,36 @@
+//
+//  STPGenericStripeObject.m
+//  Stripe
+//
+//  Created by Daniel Jackson on 7/11/18.
+//  Copyright © 2018 Stripe, Inc. All rights reserved.
+//
+
+#import "STPGenericStripeObject.h"
+
+#import "NSDictionary+Stripe.h"
+
+@interface STPGenericStripeObject ()
+@property (nonatomic, copy, readwrite) NSString *stripeId;
+@property (nonatomic, copy, readwrite) NSDictionary *allResponseFields;
+@end
+
+@implementation STPGenericStripeObject
+
++ (nullable instancetype)decodedObjectFromAPIResponse:(nullable NSDictionary *)response {
+    NSDictionary *dict = [response stp_dictionaryByRemovingNulls];
+    NSString *stripeId = [dict stp_stringForKey:@"id"];
+
+    // required fields
+    if (!stripeId) {
+        return nil;
+    }
+    STPGenericStripeObject *source = [self new];
+
+    source.stripeId = response[@"id"];
+    source.allResponseFields = dict;
+
+    return source;
+}
+
+@end
diff --git a/Tests/Tests/STPGenericStripeObjectTest.m b/Tests/Tests/STPGenericStripeObjectTest.m
new file mode 100644
index 00000000000..ed6101a6a36
--- /dev/null
+++ b/Tests/Tests/STPGenericStripeObjectTest.m
@@ -0,0 +1,27 @@
+//
+//  STPGenericStripeObjectTest.m
+//  StripeiOS Tests
+//
+//  Created by Daniel Jackson on 7/11/18.
+//  Copyright © 2018 Stripe, Inc. All rights reserved.
+//
+
+#import <XCTest/XCTest.h>
+
+#import "STPGenericStripeObject.h"
+
+@interface STPGenericStripeObjectTest : XCTestCase
+
+@end
+
+@implementation STPGenericStripeObjectTest
+
+- (void)testDecodedObject {
+    XCTAssertNil([STPGenericStripeObject decodedObjectFromAPIResponse:@{}]);
+
+    STPGenericStripeObject *obj = [STPGenericStripeObject decodedObjectFromAPIResponse:@{@"id": @"card_XYZ"}];
+    XCTAssertNotNil(obj);
+    XCTAssertEqualObjects(obj.stripeId, @"card_XYZ");
+}
+
+@end

From 155ab8d86a1a5e3030926ecb4a41d3e4f3873c05 Mon Sep 17 00:00:00 2001
From: Dan Jackson <danj@stripe.com>
Date: Wed, 11 Jul 2018 17:35:18 -0700
Subject: [PATCH 3/3] Change completion block type of private API
 `deleteSource:fromCustomerUsingKey:completion:`

When deleting a `Card`, the returned JSON does not have all the required fields for a
`STPCard` - it only has `id` and `deleted`.
When deleting/detaching a `Source`, the returned JSON *is* a decodable `Source` (and does
not have a `deleted` field).

Using `STPGenericStripeObject` as the deserializer, because it's the only thing that'll
work for this method. We cannot use multiple deserializers, because the parsing code relies
on the `object` field to pick the correct deserializer, and deleted Cards don't have an
`object`.

Since the `STPGenericStripeObject` is *not* interesting (the caller already has the id, it
was the first argument to this method), just using `STPErrorBlock` as the completion.
---
 Stripe/STPAPIClient+Private.h        |  2 +-
 Stripe/STPAPIClient.m                | 13 +++++++------
 Stripe/STPCustomerContext.m          |  2 +-
 Tests/Tests/STPCustomerContextTest.m |  4 ++--
 4 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/Stripe/STPAPIClient+Private.h b/Stripe/STPAPIClient+Private.h
index a523c93097c..2737200d16f 100644
--- a/Stripe/STPAPIClient+Private.h
+++ b/Stripe/STPAPIClient+Private.h
@@ -73,7 +73,7 @@ toCustomerUsingKey:(STPEphemeralKey *)ephemeralKey
  */
 + (void)deleteSource:(NSString *)sourceID
 fromCustomerUsingKey:(STPEphemeralKey *)ephemeralKey
-          completion:(STPSourceProtocolCompletionBlock)completion;
+          completion:(STPErrorBlock)completion;
 
 @end
 
diff --git a/Stripe/STPAPIClient.m b/Stripe/STPAPIClient.m
index ce0f6675e7e..3765c074f1b 100644
--- a/Stripe/STPAPIClient.m
+++ b/Stripe/STPAPIClient.m
@@ -15,16 +15,17 @@
 
 #import "NSBundle+Stripe_AppName.h"
 #import "NSError+Stripe.h"
-#import "STPAPIRequest.h"
+#import "NSMutableURLRequest+Stripe.h"
 #import "STPAnalyticsClient.h"
+#import "STPAPIRequest.h"
 #import "STPBankAccount.h"
 #import "STPCard.h"
 #import "STPDispatchFunctions.h"
 #import "STPEphemeralKey.h"
 #import "STPFormEncoder.h"
+#import "STPGenericStripeObject.h"
 #import "STPMultipartFormDataEncoder.h"
 #import "STPMultipartFormDataPart.h"
-#import "NSMutableURLRequest+Stripe.h"
 #import "STPPaymentConfiguration.h"
 #import "STPPaymentIntent+Private.h"
 #import "STPPaymentIntentParams.h"
@@ -542,15 +543,15 @@ + (void)addSource:(NSString *)sourceID
                                              }];
 }
 
-+ (void)deleteSource:(NSString *)sourceID fromCustomerUsingKey:(STPEphemeralKey *)ephemeralKey completion:(STPSourceProtocolCompletionBlock)completion {
++ (void)deleteSource:(NSString *)sourceID fromCustomerUsingKey:(STPEphemeralKey *)ephemeralKey completion:(STPErrorBlock)completion {
     STPAPIClient *client = [self apiClientWithEphemeralKey:ephemeralKey];
     NSString *endpoint = [NSString stringWithFormat:@"%@/%@/%@/%@", APIEndpointCustomers, ephemeralKey.customerID, APIEndpointSources, sourceID];
     [STPAPIRequest<STPSourceProtocol> deleteWithAPIClient:client
                                                  endpoint:endpoint
                                                parameters:nil
-                                            deserializers:@[[STPCard new], [STPSource new]]
-                                               completion:^(id object, __unused NSHTTPURLResponse *response, NSError *error) {
-                                                   completion(object, error);
+                                            deserializers:@[[STPGenericStripeObject new]]
+                                               completion:^(__unused STPGenericStripeObject *object, __unused NSHTTPURLResponse *response, NSError *error) {
+                                                   completion(error);
                                                }];
 }
 
diff --git a/Stripe/STPCustomerContext.m b/Stripe/STPCustomerContext.m
index 5cfb61d533a..057332d048f 100644
--- a/Stripe/STPCustomerContext.m
+++ b/Stripe/STPCustomerContext.m
@@ -194,7 +194,7 @@ - (void)detachSourceFromCustomer:(id<STPSourceProtocol>)source completion:(STPEr
 
         [STPAPIClient deleteSource:source.stripeID
               fromCustomerUsingKey:ephemeralKey
-                        completion:^(__unused id<STPSourceProtocol> obj, NSError *error) {
+                        completion:^(NSError *error) {
                             [self clearCachedCustomer];
 
                             if (completion) {
diff --git a/Tests/Tests/STPCustomerContextTest.m b/Tests/Tests/STPCustomerContextTest.m
index 77c1a938d01..f61c07ce615 100644
--- a/Tests/Tests/STPCustomerContextTest.m
+++ b/Tests/Tests/STPCustomerContextTest.m
@@ -285,9 +285,9 @@ - (void)testDetachSourceFromCustomerCallsAPIClientCorrectly {
                    fromCustomerUsingKey:[OCMArg isEqual:customerKey]
                              completion:[OCMArg any]])
     .andDo(^(NSInvocation *invocation) {
-        STPSourceProtocolCompletionBlock completion;
+        STPErrorBlock completion;
         [invocation getArgument:&completion atIndex:4];
-        completion([STPFixtures cardSource], nil);
+        completion(nil);
         [exp fulfill];
     });
     id mockKeyManager = [self mockKeyManagerWithKey:customerKey];