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:
Yuki 2019-11-25 15:59:17 -08:00 committed by GitHub
parent a1b3c7d887
commit 2b21001711
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 195 additions and 15 deletions

View File

@ -173,5 +173,66 @@ class BasicIntegrationUITests: XCTestCase {
waitToAppear(errorButton)
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)
}
}

View File

@ -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

View File

@ -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;

View File

@ -138,10 +138,12 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) BOOL loading;
/**
@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.
The Stripe ID of a payment method to display as the default pre-selected option.
Customer doesn't have a default payment method property, but you can store one (in its metadata, for example) and set this property accordingly.
@note Set this property immediately after initializing STPPaymentContext, or call `retryLoading` afterwards.
*/
@property (nonatomic, copy, nullable) NSString *defaultPaymentMethod;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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];
[strongSelf2.loadingPromise succeed:paymentTuple];
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];
}
});
}];
});

View File

@ -78,6 +78,7 @@ static const CGFloat kCheckmarkWidth = 14.f;
[self.titleLabel.centerYAnchor constraintEqualToAnchor:self.contentView.centerYAnchor],
]];
self.isAccessibilityElement = YES;
}
return self;
}
@ -120,6 +121,13 @@ static const CGFloat kCheckmarkWidth = 14.f;
// Checkmark icon
self.checkmarkIcon.tintColor = theme.accentColor;
self.checkmarkIcon.hidden = !selected;
// Accessibility
if (selected) {
self.accessibilityTraits |= UIAccessibilityTraitSelected;
} else {
self.accessibilityTraits &= ~UIAccessibilityTraitSelected;
}
[self setNeedsLayout];
}

View File

@ -71,9 +71,9 @@ NS_ASSUME_NONNULL_BEGIN
}
return [[self class] tupleWithPaymentOptions:paymentOptions
selectedPaymentOption:selectedPaymentMethod
addApplePayOption:configuration.applePayEnabled
additionalOptions:configuration.additionalPaymentOptions];
selectedPaymentOption:selectedPaymentMethod
addApplePayOption:configuration.applePayEnabled
additionalOptions:configuration.additionalPaymentOptions];
}
@end

View File

@ -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];
}

View File

@ -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;
}

View File

@ -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" \