Store last selected payment method (#1445)
* Store last selected payment method * Try to improve defaultPaymentMethod docstring * PR feedback * Update README and ci_script logs to say current test iphone version (iphone 7, 12.2) * Add stubs to STPCustomerContext mock
This commit is contained in:
parent
a1b3c7d887
commit
2b21001711
|
@ -174,4 +174,65 @@ class BasicIntegrationUITests: XCTestCase {
|
|||
errorButton.tap()
|
||||
}
|
||||
|
||||
func testPaymentOptionsDefault() {
|
||||
// Note that the example backend creates a new Customer every time you start the app
|
||||
// A STPPaymentOptionsVC w/o a selected card...
|
||||
let app = XCUIApplication()
|
||||
disableAddressEntry(app)
|
||||
selectItems(app)
|
||||
let buyNowButton = app.buttons["Buy Now"]
|
||||
buyNowButton.tap()
|
||||
let tablesQuery = app.tables
|
||||
let payFromButton = tablesQuery.otherElements.containing(.staticText, identifier:"Pay from").children(matching: .button).element
|
||||
payFromButton.tap()
|
||||
|
||||
// ...preselects Apple Pay by default
|
||||
let applePay = tablesQuery.cells["Apple Pay"]
|
||||
waitToAppear(applePay)
|
||||
XCTAssertTrue(applePay.isSelected)
|
||||
|
||||
// Selecting another payment method...
|
||||
let visa = tablesQuery.cells["Visa Ending In 3220"]
|
||||
visa.tap()
|
||||
|
||||
// ...and resetting the PaymentOptions VC...
|
||||
// Note that STPPaymentContext clears its cache and refetches every time it's initialized, which happens whenever CheckoutViewController is pushed on
|
||||
app.navigationBars["Checkout"].buttons["Products"].tap()
|
||||
buyNowButton.tap()
|
||||
payFromButton.tap()
|
||||
|
||||
// ...should keep the 3220 card selected
|
||||
XCTAssertTrue(visa.isSelected)
|
||||
XCTAssertFalse(applePay.isSelected)
|
||||
|
||||
// Reselecting Apple Pay...
|
||||
applePay.tap()
|
||||
|
||||
// ...and resetting the PaymentOptions VC...
|
||||
app.navigationBars["Checkout"].buttons["Products"].tap()
|
||||
buyNowButton.tap()
|
||||
payFromButton.tap()
|
||||
|
||||
// ...should keep Apple Pay selected
|
||||
XCTAssertTrue(applePay.isSelected)
|
||||
XCTAssertFalse(visa.isSelected)
|
||||
|
||||
// Selecting another payment method...
|
||||
visa.tap()
|
||||
|
||||
// ...and logging out...
|
||||
app.navigationBars["Checkout"].buttons["Products"].tap()
|
||||
app.navigationBars["Emoji Apparel"].buttons["Settings"].tap()
|
||||
app.tables.children(matching: .cell).element(boundBy: 16).staticTexts["Log out"].tap()
|
||||
app.navigationBars["Settings"].buttons["Done"].tap()
|
||||
|
||||
// ...and going back to PaymentOptionsVC...
|
||||
buyNowButton.tap()
|
||||
payFromButton.tap()
|
||||
|
||||
// ..should not retain the visa default
|
||||
waitToAppear(applePay)
|
||||
XCTAssertTrue(applePay.isSelected)
|
||||
XCTAssertFalse(visa.isSelected)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,7 +104,7 @@ We welcome contributions of any kind including new features, bug fixes, and docu
|
|||
1. Install Carthage (if you have homebrew installed, `brew install carthage`)
|
||||
2. From the root of the repo, install test dependencies by running `carthage bootstrap --platform ios --configuration Release --no-use-binaries`
|
||||
3. Open Stripe.xcworkspace
|
||||
4. Choose the "StripeiOS" scheme with the iPhone 6, iOS 11.2 simulator (required for snapshot tests to pass)
|
||||
4. Choose the "StripeiOS" scheme with the iPhone 7, iOS 12.2 simulator (required for snapshot tests to pass)
|
||||
5. Run Product -> Test
|
||||
|
||||
## Migrating from Older Versions
|
||||
|
|
|
@ -1925,6 +1925,7 @@
|
|||
B64763B222FE193800C01BC0 /* STPSetupIntentLastSetupError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = STPSetupIntentLastSetupError.h; path = PublicHeaders/STPSetupIntentLastSetupError.h; sourceTree = "<group>"; };
|
||||
B64763B322FE193800C01BC0 /* STPSetupIntentLastSetupError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPSetupIntentLastSetupError.m; sourceTree = "<group>"; };
|
||||
B64763B822FE1AF700C01BC0 /* STPSetupIntentLastSetupErrorTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPSetupIntentLastSetupErrorTest.m; sourceTree = "<group>"; };
|
||||
B65D7C632384ACDD000C6D34 /* STPCustomerContext+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "STPCustomerContext+Private.h"; sourceTree = "<group>"; };
|
||||
B664D64722B800AF00E6354B /* STPThreeDSButtonCustomization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPThreeDSButtonCustomization.m; sourceTree = "<group>"; };
|
||||
B664D64A22B8034D00E6354B /* STPThreeDSCustomization+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "STPThreeDSCustomization+Private.h"; sourceTree = "<group>"; };
|
||||
B664D64D22B8085900E6354B /* STPThreeDSUICustomization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPThreeDSUICustomization.m; sourceTree = "<group>"; };
|
||||
|
@ -3153,6 +3154,7 @@
|
|||
F1728CF41EAAA457002E0C29 /* PaymentContext */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B65D7C632384ACDD000C6D34 /* STPCustomerContext+Private.h */,
|
||||
04E39F5A1CECFAFD00AF3B96 /* STPPaymentContext+Private.h */,
|
||||
);
|
||||
name = PaymentContext;
|
||||
|
|
|
@ -138,9 +138,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
@property (nonatomic, readonly) BOOL loading;
|
||||
|
||||
/**
|
||||
The Stripe ID of a payment method to display as the default pre-selected option.
|
||||
@note This is no longer recommended as of v18.3.0 - the SDK automatically saves the Stripe ID of the last selected
|
||||
payment method using NSUserDefaults and displays it as the default pre-selected option. You can override this behavior
|
||||
by setting this property.
|
||||
|
||||
Customer doesn't have a default payment method property, but you can store one (in its metadata, for example) and set this property accordingly.
|
||||
The Stripe ID of a payment method to display as the default pre-selected option.
|
||||
|
||||
@note Set this property immediately after initializing STPPaymentContext, or call `retryLoading` afterwards.
|
||||
*/
|
||||
|
|
|
@ -102,10 +102,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
@property (nonatomic, strong, nullable) STPUserInformation *prefilledInformation;
|
||||
|
||||
/**
|
||||
The Stripe ID of a payment method to display as the default pre-selected option.
|
||||
@note This is no longer recommended as of v18.3.0 - the SDK automatically saves the Stripe ID of the last selected
|
||||
payment method using NSUserDefaults and displays it as the default pre-selected option. You can override this behavior
|
||||
by setting this property.
|
||||
|
||||
Customer doesn't have a default payment method property, but you can store one
|
||||
(in its metadata, for example) and set this property accordingly.
|
||||
The Stripe ID of a payment method to display as the default pre-selected option.
|
||||
|
||||
@note Setting this after the view controller's view has loaded has no effect.
|
||||
*/
|
||||
|
@ -160,6 +161,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
@end
|
||||
|
||||
#pragma mark - STPPaymentOptionsViewControllerDelegate
|
||||
|
||||
/**
|
||||
An `STPPaymentOptionsViewControllerDelegate` responds when a user selects a
|
||||
payment option from (or cancels) an `STPPaymentOptionsViewController`. In both
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// STPCustomerContext+Private.h
|
||||
// Stripe
|
||||
//
|
||||
// Created by Yuki Tokuhiro on 11/19/19.
|
||||
// Copyright © 2019 Stripe, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Stripe/Stripe.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface STPCustomerContext ()
|
||||
|
||||
- (void)saveLastSelectedPaymentMethodIDForCustomer:(nullable NSString *)paymentMethodID completion:(nullable STPErrorBlock)completion;
|
||||
|
||||
- (void)retrieveLastSelectedPaymentMethodIDForCustomerWithCompletion:(void (^)(NSString * _Nullable, NSError * _Nullable))completion;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -7,6 +7,7 @@
|
|||
//
|
||||
|
||||
#import "STPCustomerContext.h"
|
||||
#import "STPCustomerContext+Private.h"
|
||||
|
||||
#import "STPAPIClient+Private.h"
|
||||
#import "STPCustomer+Private.h"
|
||||
|
@ -17,6 +18,9 @@
|
|||
#import "STPPaymentMethodCardWallet.h"
|
||||
#import "STPDispatchFunctions.h"
|
||||
|
||||
/// Stores the key we use in NSUserDefaults to save a dictionary of Customer id to their last selected payment method ID
|
||||
static NSString *const kLastSelectedPaymentMethodDefaultsKey = @"com.stripe.lib:STPStripeCustomerToLastSelectedPaymentMethodKey";
|
||||
|
||||
static NSTimeInterval const CachedCustomerMaxAge = 60;
|
||||
|
||||
@interface STPCustomerContext ()
|
||||
|
@ -255,4 +259,46 @@ static NSTimeInterval const CachedCustomerMaxAge = 60;
|
|||
}];
|
||||
}
|
||||
|
||||
- (void)saveLastSelectedPaymentMethodIDForCustomer:(NSString *)paymentMethodID completion:(nullable STPErrorBlock)completion {
|
||||
[self.keyManager getOrCreateKey:^(STPEphemeralKey *ephemeralKey, NSError *retrieveKeyError) {
|
||||
if (retrieveKeyError) {
|
||||
if (completion) {
|
||||
stpDispatchToMainThreadIfNecessary(^{
|
||||
completion(retrieveKeyError);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableDictionary<NSString *, NSString *>* customerToDefaultPaymentMethodID = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:kLastSelectedPaymentMethodDefaultsKey] mutableCopy] ?: [NSMutableDictionary new];
|
||||
NSString *customerID = ephemeralKey.customerID;
|
||||
|
||||
customerToDefaultPaymentMethodID[customerID] = [paymentMethodID copy];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:customerToDefaultPaymentMethodID forKey:kLastSelectedPaymentMethodDefaultsKey];
|
||||
if (completion) {
|
||||
stpDispatchToMainThreadIfNecessary(^{
|
||||
completion(nil);
|
||||
});
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)retrieveLastSelectedPaymentMethodIDForCustomerWithCompletion:(void (^)(NSString * _Nullable, NSError * _Nullable))completion {
|
||||
[self.keyManager getOrCreateKey:^(STPEphemeralKey *ephemeralKey, NSError *retrieveKeyError) {
|
||||
if (retrieveKeyError) {
|
||||
if (completion) {
|
||||
stpDispatchToMainThreadIfNecessary(^{
|
||||
completion(nil, retrieveKeyError);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary<NSString *, NSString *>* customerToDefaultPaymentMethodID = [[NSUserDefaults standardUserDefaults] dictionaryForKey:kLastSelectedPaymentMethodDefaultsKey];
|
||||
stpDispatchToMainThreadIfNecessary(^{
|
||||
completion(customerToDefaultPaymentMethodID[ephemeralKey.customerID], nil);
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
#import "PKPaymentAuthorizationViewController+Stripe_Blocks.h"
|
||||
#import "STPAddCardViewController+Private.h"
|
||||
#import "STPCustomerContext.h"
|
||||
#import "STPCustomerContext+Private.h"
|
||||
#import "STPDispatchFunctions.h"
|
||||
#import "STPPaymentConfiguration+Private.h"
|
||||
#import "STPPaymentContext+Private.h"
|
||||
|
@ -157,8 +157,18 @@ typedef NS_ENUM(NSUInteger, STPPaymentContextState) {
|
|||
[strongSelf2.loadingPromise fail:error];
|
||||
return;
|
||||
}
|
||||
STPPaymentOptionTuple *paymentTuple = [STPPaymentOptionTuple tupleFilteredForUIWithPaymentMethods:paymentMethods selectedPaymentMethod:strongSelf2.defaultPaymentMethod configuration:strongSelf2.configuration];
|
||||
|
||||
if (self.defaultPaymentMethod == nil && [strongSelf2.apiAdapter isKindOfClass:[STPCustomerContext class]]) {
|
||||
// Retrieve the last selected payment method saved by STPCustomerContext
|
||||
[((STPCustomerContext *)strongSelf2.apiAdapter) retrieveLastSelectedPaymentMethodIDForCustomerWithCompletion:^(NSString * _Nullable paymentMethodID, NSError * _Nullable __unused _) {
|
||||
__strong typeof(self) strongSelf3 = weakSelf;
|
||||
STPPaymentOptionTuple *paymentTuple = [STPPaymentOptionTuple tupleFilteredForUIWithPaymentMethods:paymentMethods selectedPaymentMethod:paymentMethodID configuration:strongSelf3.configuration];
|
||||
[strongSelf3.loadingPromise succeed:paymentTuple];
|
||||
}];
|
||||
} else {
|
||||
STPPaymentOptionTuple *paymentTuple = [STPPaymentOptionTuple tupleFilteredForUIWithPaymentMethods:paymentMethods selectedPaymentMethod:self.defaultPaymentMethod configuration:strongSelf2.configuration];
|
||||
[strongSelf2.loadingPromise succeed:paymentTuple];
|
||||
}
|
||||
});
|
||||
}];
|
||||
});
|
||||
|
|
|
@ -78,6 +78,7 @@ static const CGFloat kCheckmarkWidth = 14.f;
|
|||
[self.titleLabel.centerYAnchor constraintEqualToAnchor:self.contentView.centerYAnchor],
|
||||
|
||||
]];
|
||||
self.isAccessibilityElement = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
@ -121,6 +122,13 @@ static const CGFloat kCheckmarkWidth = 14.f;
|
|||
self.checkmarkIcon.tintColor = theme.accentColor;
|
||||
self.checkmarkIcon.hidden = !selected;
|
||||
|
||||
// Accessibility
|
||||
if (selected) {
|
||||
self.accessibilityTraits |= UIAccessibilityTraitSelected;
|
||||
} else {
|
||||
self.accessibilityTraits &= ~UIAccessibilityTraitSelected;
|
||||
}
|
||||
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
#import "STPCard.h"
|
||||
#import "STPColorUtils.h"
|
||||
#import "STPCoreViewController+Private.h"
|
||||
#import "STPCustomerContext+Private.h"
|
||||
#import "STPDispatchFunctions.h"
|
||||
#import "STPLocalizationUtils.h"
|
||||
#import "STPPaymentActivityIndicatorView.h"
|
||||
|
@ -80,7 +81,15 @@
|
|||
if (error) {
|
||||
[promise fail:error];
|
||||
} else {
|
||||
STPPaymentOptionTuple *paymentTuple = [STPPaymentOptionTuple tupleFilteredForUIWithPaymentMethods:paymentMethods selectedPaymentMethod:self.defaultPaymentMethod configuration:configuration];
|
||||
NSString *defaultPaymentMethod = self.defaultPaymentMethod;
|
||||
if (defaultPaymentMethod == nil && [apiAdapter isKindOfClass:[STPCustomerContext class]]) {
|
||||
// Retrieve the last selected payment method saved by STPCustomerContext
|
||||
[((STPCustomerContext *)apiAdapter) retrieveLastSelectedPaymentMethodIDForCustomerWithCompletion:^(NSString * _Nullable paymentMethodID, NSError * _Nullable __unused _) {
|
||||
STPPaymentOptionTuple *paymentTuple = [STPPaymentOptionTuple tupleFilteredForUIWithPaymentMethods:paymentMethods selectedPaymentMethod:paymentMethodID configuration:configuration];
|
||||
[promise succeed:paymentTuple];
|
||||
}];
|
||||
}
|
||||
STPPaymentOptionTuple *paymentTuple = [STPPaymentOptionTuple tupleFilteredForUIWithPaymentMethods:paymentMethods selectedPaymentMethod:defaultPaymentMethod configuration:configuration];
|
||||
[promise succeed:paymentTuple];
|
||||
}
|
||||
});
|
||||
|
@ -166,6 +175,19 @@
|
|||
}
|
||||
|
||||
- (void)finishWithPaymentOption:(id<STPPaymentOption>)paymentOption {
|
||||
BOOL isReusablePaymentMethod = [paymentOption isKindOfClass:[STPPaymentMethod class]] && ((STPPaymentMethod *)paymentOption).isReusable;
|
||||
|
||||
if ([self.apiAdapter isKindOfClass:[STPCustomerContext class]]) {
|
||||
if (isReusablePaymentMethod) {
|
||||
// Save the payment method
|
||||
STPPaymentMethod *paymentMethod = (STPPaymentMethod *)paymentOption;
|
||||
[((STPCustomerContext *)self.apiAdapter) saveLastSelectedPaymentMethodIDForCustomer:paymentMethod.stripeId completion:nil];
|
||||
} else {
|
||||
// The customer selected something else (like Apple Pay)
|
||||
[((STPCustomerContext *)self.apiAdapter) saveLastSelectedPaymentMethodIDForCustomer:nil completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(paymentOptionsViewController:didSelectPaymentOption:)]) {
|
||||
[self.delegate paymentOptionsViewController:self didSelectPaymentOption:paymentOption];
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#import "STPFixtures.h"
|
||||
#import "STPPaymentConfiguration+Private.h"
|
||||
#import "STPPaymentContext+Private.h"
|
||||
#import "STPCustomerContext+Private.h"
|
||||
#import "UIViewController+Stripe_Promises.h"
|
||||
|
||||
@interface STPPaymentConfiguration (STPMocks)
|
||||
|
@ -58,6 +59,11 @@
|
|||
completion(paymentMethods, nil);
|
||||
});
|
||||
OCMStub([mock attachPaymentMethodToCustomer:[OCMArg any] completion:[OCMArg invokeBlock]]);
|
||||
OCMStub([mock retrieveLastSelectedPaymentMethodIDForCustomerWithCompletion:[OCMArg any]]).andDo(^(NSInvocation *invocation){
|
||||
void (^completion)(NSString *, NSError *);
|
||||
[invocation getArgument:&completion atIndex:2];
|
||||
completion(nil, nil);
|
||||
});
|
||||
return mock;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,8 +19,8 @@ if ! command -v xcpretty > /dev/null; then
|
|||
gem install xcpretty --no-document || die "Executing \`gem install xcpretty\` failed"
|
||||
fi
|
||||
|
||||
# Execute sample app builds (iPhone 6, iOS 11.x)
|
||||
info "Executing sample app builds (iPhone 6, iOS 11.x)..."
|
||||
# Execute sample app builds
|
||||
info "Executing sample app builds (iPhone 7, iOS 12.2)..."
|
||||
|
||||
xcodebuild build \
|
||||
-workspace "Stripe.xcworkspace" \
|
||||
|
|
Loading…
Reference in New Issue