Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store last selected payment method #1445

Merged
merged 5 commits into from
Nov 25, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions Example/BasicIntegrationUITests/BasicIntegrationUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
2 changes: 2 additions & 0 deletions Stripe.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1924,6 +1924,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>"; };
Expand Down Expand Up @@ -3152,6 +3153,7 @@
F1728CF41EAAA457002E0C29 /* PaymentContext */ = {
isa = PBXGroup;
children = (
B65D7C632384ACDD000C6D34 /* STPCustomerContext+Private.h */,
04E39F5A1CECFAFD00AF3B96 /* STPPaymentContext+Private.h */,
);
name = PaymentContext;
Expand Down
6 changes: 5 additions & 1 deletion Stripe/PublicHeaders/STPPaymentOptionsViewController.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,13 @@ NS_ASSUME_NONNULL_BEGIN

/**
The Stripe ID of a payment method to display as the default pre-selected option.
yuki-stripe marked this conversation as resolved.
Show resolved Hide resolved

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 Setting this after the view controller's view has loaded has no effect.
@note This class automatically saves the Stripe ID of the last selected payment method using NSUserDefaults
and displays it as the default pre-selected option. Settting this value overrides that behavior.
*/
@property (nonatomic, copy, nullable) NSString *defaultPaymentMethod;

Expand Down Expand Up @@ -160,6 +162,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
Expand Down
20 changes: 20 additions & 0 deletions Stripe/STPCustomerContext+Private.h
Original file line number Diff line number Diff line change
@@ -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
46 changes: 46 additions & 0 deletions Stripe/STPCustomerContext.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

#import "STPCustomerContext.h"
#import "STPCustomerContext+Private.h"

#import "STPAPIClient+Private.h"
#import "STPCustomer+Private.h"
Expand All @@ -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 ()
Expand Down Expand Up @@ -255,4 +259,46 @@ - (void)listPaymentMethodsForCustomerWithCompletion:(STPPaymentMethodsCompletion
}];
}

- (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
16 changes: 13 additions & 3 deletions Stripe/STPPaymentContext.m
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -157,8 +157,18 @@ - (void)retryLoading {
[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];
}
});
}];
});
Expand Down
8 changes: 8 additions & 0 deletions Stripe/STPPaymentOptionTableViewCell.m
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(nulla
[self.titleLabel.centerYAnchor constraintEqualToAnchor:self.contentView.centerYAnchor],

]];
self.isAccessibilityElement = YES;
}
return self;
}
Expand Down Expand Up @@ -120,6 +121,13 @@ - (void)configureWithPaymentOption:(id<STPPaymentOption>)paymentOption theme:(ST
// Checkmark icon
self.checkmarkIcon.tintColor = theme.accentColor;
self.checkmarkIcon.hidden = !selected;

// Accessibility
if (selected) {
self.accessibilityTraits |= UIAccessibilityTraitSelected;
} else {
self.accessibilityTraits &= ~UIAccessibilityTraitSelected;
}

[self setNeedsLayout];
}
Expand Down
6 changes: 3 additions & 3 deletions Stripe/STPPaymentOptionTuple.m
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ + (instancetype)tupleFilteredForUIWithPaymentMethods:(NSArray<STPPaymentMethod *
}

return [[self class] tupleWithPaymentOptions:paymentOptions
selectedPaymentOption:selectedPaymentMethod
addApplePayOption:configuration.applePayEnabled
additionalOptions:configuration.additionalPaymentOptions];
selectedPaymentOption:selectedPaymentMethod
addApplePayOption:configuration.applePayEnabled
additionalOptions:configuration.additionalPaymentOptions];
}

@end
Expand Down
24 changes: 23 additions & 1 deletion Stripe/STPPaymentOptionsViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -80,7 +81,15 @@ - (instancetype)initWithConfiguration:(STPPaymentConfiguration *)configuration
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];
}
});
Expand Down Expand Up @@ -166,6 +175,19 @@ - (void)updateAppearance {
}

- (void)finishWithPaymentOption:(id<STPPaymentOption>)paymentOption {
BOOL isReusablePaymentMethod = [paymentOption isKindOfClass:[STPPaymentMethod class]] && ((STPPaymentMethod *)paymentOption).type == STPPaymentMethodTypeCard;
yuki-stripe marked this conversation as resolved.
Show resolved Hide resolved

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];
}
Expand Down