Skip to content

Commit

Permalink
Read the default_payment_method field from elements session and use t…
Browse files Browse the repository at this point in the history
…hat as the default PM (#4313)

## Summary
<!-- Simple summary of what was changed. -->
Added allowsSetAsDefaultPM flag. Only shown if we're using customer
session.
If allowsSetAsDefaultPM is on, then instead of reading from the locally
stored default, we read from ElementsSession.
## Motivation
<!-- Why are you making this change? If it's for fixing a bug, if
possible, please include a code snippet or example project that
demonstrates the issue. -->
[MOBILESDK-2799](https://jira.corp.stripe.com/browse/MOBILESDK-2799)
## Testing
<!-- How was the code tested? Be as specific as possible. -->
customer_session, allowsSetAsDefaultPM on
Hardcoded the value that was "read" from default_payment_method (set to
the 4242 Visa stripe id)
After changing the selected payment method and paying with it, upon
reload the PaymentSheet default is back to our hardcoded default.



https://github.com/user-attachments/assets/38601a0e-513b-40a8-a3d8-f30069723f97



## Changelog


<!-- Is this a notable change that affects users? If so, add a line to
`CHANGELOG.md` and prefix the line with one of the following:
    - [Added] for new features.
    - [Changed] for changes in existing functionality.
    - [Deprecated] for soon-to-be removed features.
    - [Removed] for now removed features.
    - [Fixed] for any bug fixes.
    - [Security] in case of vulnerabilities.
-->
  • Loading branch information
joyceqin-stripe authored Dec 9, 2024
1 parent c7ec8d8 commit 171edb3
Show file tree
Hide file tree
Showing 25 changed files with 260 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ struct CustomerSheetTestPlayground: View {
SettingPickerView(setting: $playgroundController.settings.paymentMethodRemove)
SettingPickerView(setting: $playgroundController.settings.paymentMethodRemoveLast)
SettingPickerView(setting: $playgroundController.settings.paymentMethodAllowRedisplayFilters)
SettingPickerView(setting: $playgroundController.settings.allowsSetAsDefaultPM)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//

import Combine
@_spi(STP) @_spi(CustomerSessionBetaAccess) @_spi(CardBrandFilteringBeta) import StripePaymentSheet
@_spi(STP) @_spi(CustomerSessionBetaAccess) @_spi(CardBrandFilteringBeta) @_spi(AllowsSetAsDefaultPM) import StripePaymentSheet
import SwiftUI

class CustomerSheetTestPlaygroundController: ObservableObject {
Expand Down Expand Up @@ -147,6 +147,7 @@ class CustomerSheetTestPlaygroundController: ObservableObject {
case .allowVisa:
configuration.cardBrandAcceptance = .allowed(brands: [.visa])
}
configuration.allowsSetAsDefaultPM = settings.allowsSetAsDefaultPM == .on
return configuration
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ public struct CustomerSheetTestPlaygroundSettings: Codable, Equatable {
case allowVisa
}

enum AllowsSetAsDefaultPM: String, PickerEnum {
static let enumName: String = "allowsSetAsDefaultPM"
case on
case off
}

var customerMode: CustomerMode
var customerId: String?
var customerKeyType: CustomerKeyType
Expand All @@ -169,6 +175,7 @@ public struct CustomerSheetTestPlaygroundSettings: Codable, Equatable {
var paymentMethodRemoveLast: PaymentMethodRemoveLast
var paymentMethodAllowRedisplayFilters: PaymentMethodAllowRedisplayFilters
var cardBrandAcceptance: CardBrandAcceptance
var allowsSetAsDefaultPM: AllowsSetAsDefaultPM

static func defaultValues() -> CustomerSheetTestPlaygroundSettings {
return CustomerSheetTestPlaygroundSettings(customerMode: .new,
Expand All @@ -190,7 +197,8 @@ public struct CustomerSheetTestPlaygroundSettings: Codable, Equatable {
paymentMethodRemove: .enabled,
paymentMethodRemoveLast: .enabled,
paymentMethodAllowRedisplayFilters: .always,
cardBrandAcceptance: .all)
cardBrandAcceptance: .all,
allowsSetAsDefaultPM: .off)
}

var base64Data: String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ struct PaymentSheetTestPlayground: View {
if playgroundController.settings.paymentMethodRedisplay == .enabled {
SettingPickerView(setting: $playgroundController.settings.paymentMethodAllowRedisplayFilters)
}
SettingPickerView(setting: $playgroundController.settings.allowsSetAsDefaultPM)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,12 @@ struct PaymentSheetTestPlaygroundSettings: Codable, Equatable {
case allowVisa
}

enum AllowsSetAsDefaultPM: String, PickerEnum {
static let enumName: String = "allowsSetAsDefaultPM"
case on
case off
}

var uiStyle: UIStyle
var layout: Layout
var mode: Mode
Expand Down Expand Up @@ -490,6 +496,7 @@ struct PaymentSheetTestPlaygroundSettings: Codable, Equatable {
var formSheetAction: FormSheetAction
var embeddedViewDisplaysMandateText: DisplaysMandateTextEnabled
var cardBrandAcceptance: CardBrandAcceptance
var allowsSetAsDefaultPM: AllowsSetAsDefaultPM

static func defaultValues() -> PaymentSheetTestPlaygroundSettings {
return PaymentSheetTestPlaygroundSettings(
Expand Down Expand Up @@ -535,7 +542,8 @@ struct PaymentSheetTestPlaygroundSettings: Codable, Equatable {
collectAddress: .automatic,
formSheetAction: .confirm,
embeddedViewDisplaysMandateText: .on,
cardBrandAcceptance: .all)
cardBrandAcceptance: .all,
allowsSetAsDefaultPM: .off)
}

static let nsUserDefaultsKey = "PaymentSheetTestPlaygroundSettings"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Contacts
import PassKit
@_spi(STP) import StripeCore
@_spi(STP) import StripePayments
@_spi(CustomerSessionBetaAccess) @_spi(STP) @_spi(PaymentSheetSkipConfirmation) @_spi(ExperimentalAllowsRemovalOfLastSavedPaymentMethodAPI) @_spi(EmbeddedPaymentElementPrivateBeta) @_spi(CardBrandFilteringBeta) import StripePaymentSheet
@_spi(CustomerSessionBetaAccess) @_spi(STP) @_spi(PaymentSheetSkipConfirmation) @_spi(ExperimentalAllowsRemovalOfLastSavedPaymentMethodAPI) @_spi(EmbeddedPaymentElementPrivateBeta) @_spi(CardBrandFilteringBeta) @_spi(AllowsSetAsDefaultPM) import StripePaymentSheet
import SwiftUI
import UIKit

Expand Down Expand Up @@ -184,6 +184,7 @@ class PlaygroundController: ObservableObject {
case .allowVisa:
configuration.cardBrandAcceptance = .allowed(brands: [.visa])
}
configuration.allowsSetAsDefaultPM = settings.allowsSetAsDefaultPM == .on
return configuration
}

Expand Down Expand Up @@ -271,6 +272,7 @@ class PlaygroundController: ObservableObject {
case .allowVisa:
configuration.cardBrandAcceptance = .allowed(brands: [.visa])
}
configuration.allowsSetAsDefaultPM = settings.allowsSetAsDefaultPM == .on
return configuration
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,14 @@ struct ElementsCustomer: Equatable, Hashable {
let defaultPaymentMethod = response["default_payment_method"] as? String
return ElementsCustomer(paymentMethods: paymentMethods, defaultPaymentMethod: defaultPaymentMethod, customerSession: customerSession)
}

func getDefaultOrFirstPaymentMethod() -> STPPaymentMethod? {
// if customer has a default payment method from the elements session, return the default payment method
let defaultSavedPaymentMethod = paymentMethods.first { $0.stripeId == defaultPaymentMethod }
if let defaultSavedPaymentMethod = defaultSavedPaymentMethod {
return defaultSavedPaymentMethod
}
// otherwise, return the first payment method from the customer's list of saved payment methods
return paymentMethods.first
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,14 @@ extension CustomerSessionAdapter {
return stripePaymentMethodId
}

func fetchSelectedPaymentOption(for customerId: String) -> CustomerPaymentOption? {
func fetchSelectedPaymentOption(for customerId: String, customer: ElementsCustomer? = nil) -> CustomerPaymentOption? {
// if opted in to the "set as default" feature, try to get default payment method from elements session
if configuration.allowsSetAsDefaultPM {
guard let customer = customer,
let defaultPaymentMethod = customer.getDefaultOrFirstPaymentMethod() else { return nil }
return CustomerPaymentOption.stripeId(defaultPaymentMethod.stripeId)
}

return CustomerPaymentOption.defaultPaymentMethod(for: customerId)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,18 @@ extension CustomerSheet {
switch customerSheetDataSource.dataSource {
case .customerSession(let customerSessionAdapter):
let (elementsSession, customerSessionClientSecret) = try await customerSessionAdapter.elementsSessionWithCustomerSessionClientSecret()
let selectedPaymentOption = CustomerPaymentOption.defaultPaymentMethod(for: customerSessionClientSecret.customerId)

var selectedPaymentOption: CustomerPaymentOption?

// if opted in to the "set as default" feature, try to get default payment method from elements session
if configuration.allowsSetAsDefaultPM {
guard let customer = elementsSession.customer,
let defaultPaymentMethod = customer.getDefaultOrFirstPaymentMethod() else { return nil }
selectedPaymentOption = CustomerPaymentOption.stripeId(defaultPaymentMethod.stripeId)
}
else {
selectedPaymentOption = CustomerPaymentOption.defaultPaymentMethod(for: customerSessionClientSecret.customerId)
}

switch selectedPaymentOption {
case .applePay:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ extension CustomerSheet {
/// Note: Card brand filtering is not currently supported by Link.
@_spi(CardBrandFilteringBeta) public var cardBrandAcceptance: PaymentSheet.CardBrandAcceptance = .all

/// This is an experimental feature that may be removed at any time.
/// If true, users can set a payment method as default and sync their default payment method across web and mobile
/// If false (default), users cannot set default payment methods.
@_spi(AllowsSetAsDefaultPM) public var allowsSetAsDefaultPM = false

public init () {
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ class CustomerSheetDataSource {
// Ensure local specs are loaded prior to the ones from elementSession
await loadFormSpecs()
let customerId = try await customerSessionClientSecret.customerId
let paymentOption = customerSessionAdapter.fetchSelectedPaymentOption(for: customerId)
let elementSession = try await elementsSessionResult
let paymentOption = customerSessionAdapter.fetchSelectedPaymentOption(for: customerId, customer: elementSession.customer)

// Override with specs from elementSession
_ = FormSpecProvider.shared.loadFrom(elementSession.paymentMethodSpecs as Any)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,16 @@ extension EmbeddedPaymentElement {
}

// If there's no previous customer input, default to the customer's default or the first saved payment method, if any
let customerDefault = CustomerPaymentOption.defaultPaymentMethod(for: configuration.customer?.id)
var customerDefault: CustomerPaymentOption?
// if opted in to the "set as default" feature, try to get default payment method from elements session
if configuration.allowsSetAsDefaultPM {
if let defaultPaymentMethod = loadResult.elementsSession.customer?.getDefaultOrFirstPaymentMethod() {
customerDefault = CustomerPaymentOption.stripeId(defaultPaymentMethod.stripeId)
}
}
else {
customerDefault = CustomerPaymentOption.defaultPaymentMethod(for: configuration.customer?.id)
}
switch customerDefault {
case .applePay:
return .applePay
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ extension EmbeddedPaymentElement {
/// Note: Card brand filtering is not currently supported by Link.
@_spi(CardBrandFilteringBeta) public var cardBrandAcceptance: PaymentSheet.CardBrandAcceptance = .all

/// This is an experimental feature that may be removed at any time.
/// If true, users can set a payment method as default and sync their default payment method across web and mobile
/// If false (default), users cannot set default payment methods.
@_spi(AllowsSetAsDefaultPM) public var allowsSetAsDefaultPM = false

/// The view can display payment methods like “Card” that, when tapped, open a form sheet where customers enter their payment method details. The sheet has a button at the bottom. `FormSheetAction` enumerates the actions the button can perform.
public enum FormSheetAction {
/// The button says “Pay” or “Setup”. When tapped, we confirm the payment or setup in the form sheet.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ protocol PaymentElementConfiguration: PaymentMethodRequirementProvider {
var cardBrandAcceptance: PaymentSheet.CardBrandAcceptance { get set }
var analyticPayload: [String: Any] { get }
var disableWalletPaymentMethodFiltering: Bool { get set }
var allowsSetAsDefaultPM: Bool { get set }
var linkPaymentMethodsOnly: Bool { get set }
var forceNativeLinkEnabled: Bool { get set }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ extension PaymentSheet {
/// Note: Card brand filtering is not currently supported by Link.
@_spi(CardBrandFilteringBeta) public var cardBrandAcceptance: PaymentSheet.CardBrandAcceptance = .all

/// This is an experimental feature that may be removed at any time.
/// If true, users can set a payment method as default and sync their default payment method across web and mobile
/// If false (default), users cannot set default payment methods.
@_spi(AllowsSetAsDefaultPM) public var allowsSetAsDefaultPM = false
}

/// Defines the layout orientations available for displaying payment methods in PaymentSheet.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ final class PaymentSheetLoader {
savedPaymentMethods: filteredSavedPaymentMethods,
customerID: configuration.customer?.id,
showApplePay: integrationShape.canDefaultToLinkOrApplePay ? isApplePayEnabled : false,
showLink: integrationShape.canDefaultToLinkOrApplePay ? isLinkEnabled : false
showLink: integrationShape.canDefaultToLinkOrApplePay ? isLinkEnabled : false,
allowsSetAsDefaultPM: configuration.allowsSetAsDefaultPM,
customer: elementsSession.customer
)
let paymentMethodTypes = PaymentSheet.PaymentMethodType.filteredPaymentMethodTypes(from: intent, elementsSession: elementsSession, configuration: configuration, logAvailability: true)

Expand Down Expand Up @@ -316,9 +318,18 @@ final class PaymentSheetLoader {

// Move default PM to front
if let customerID = configuration.customer?.id {
let defaultPaymentMethod = CustomerPaymentOption.defaultPaymentMethod(for: customerID)
var defaultPaymentMethodOption: CustomerPaymentOption?
// if opted in to the "set as default" feature, try to get default payment method from elements session
if configuration.allowsSetAsDefaultPM {
guard let customer = elementsSession.customer,
let defaultPaymentMethod = customer.getDefaultOrFirstPaymentMethod() else { return [] }
defaultPaymentMethodOption = CustomerPaymentOption.stripeId(defaultPaymentMethod.stripeId)
}
else {
defaultPaymentMethodOption = CustomerPaymentOption.defaultPaymentMethod(for: customerID)
}
if let defaultPMIndex = savedPaymentMethods.firstIndex(where: {
$0.stripeId == defaultPaymentMethod?.value
$0.stripeId == defaultPaymentMethodOption?.value
}) {
let defaultPM = savedPaymentMethods.remove(at: defaultPMIndex)
savedPaymentMethods.insert(defaultPM, at: 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class SavedPaymentOptionsViewController: UIViewController {
let isTestMode: Bool
let allowsRemovalOfLastSavedPaymentMethod: Bool
let allowsRemovalOfPaymentMethods: Bool
let allowsSetAsDefaultPM: Bool
}

// MARK: - Internal Properties
Expand Down Expand Up @@ -216,6 +217,7 @@ class SavedPaymentOptionsViewController: UIViewController {
}
weak var delegate: SavedPaymentOptionsViewControllerDelegate?
var appearance = PaymentSheet.Appearance.default
var elementsSession: STPElementsSession

// MARK: - Private Properties
private var selectedViewModelIndex: Int?
Expand Down Expand Up @@ -311,6 +313,7 @@ class SavedPaymentOptionsViewController: UIViewController {
paymentSheetConfiguration: PaymentSheet.Configuration,
intent: Intent,
appearance: PaymentSheet.Appearance,
elementsSession: STPElementsSession,
cbcEligible: Bool = false,
analyticsHelper: PaymentSheetAnalyticsHelper,
delegate: SavedPaymentOptionsViewControllerDelegate? = nil
Expand All @@ -320,6 +323,7 @@ class SavedPaymentOptionsViewController: UIViewController {
self.paymentSheetConfiguration = paymentSheetConfiguration
self.intent = intent
self.appearance = appearance
self.elementsSession = elementsSession
self.cbcEligible = cbcEligible
self.delegate = delegate
self.analyticsHelper = analyticsHelper
Expand Down Expand Up @@ -363,7 +367,9 @@ class SavedPaymentOptionsViewController: UIViewController {
savedPaymentMethods: savedPaymentMethods,
customerID: configuration.customerID,
showApplePay: configuration.showApplePay,
showLink: configuration.showLink
showLink: configuration.showLink,
allowsSetAsDefaultPM: configuration.allowsSetAsDefaultPM,
customer: elementsSession.customer
)

collectionView.reloadData()
Expand Down Expand Up @@ -437,9 +443,19 @@ class SavedPaymentOptionsViewController: UIViewController {

/// Creates the list of viewmodels to display in the "saved payment methods" carousel e.g. `["+ Add", "Apple Pay", "Link", "Visa 4242"]`
/// - Returns defaultSelectedIndex: The index of the view model that is the default e.g. in the above list, if "Visa 4242" is the default, the index is 3.
static func makeViewModels(savedPaymentMethods: [STPPaymentMethod], customerID: String?, showApplePay: Bool, showLink: Bool) -> (defaultSelectedIndex: Int, viewModels: [Selection]) {
static func makeViewModels(savedPaymentMethods: [STPPaymentMethod], customerID: String?, showApplePay: Bool, showLink: Bool, allowsSetAsDefaultPM: Bool, customer: ElementsCustomer?) -> (defaultSelectedIndex: Int, viewModels: [Selection]) {
// Get the default
let defaultPaymentMethod = CustomerPaymentOption.defaultPaymentMethod(for: customerID)
var defaultPaymentMethodOption: CustomerPaymentOption?
// if opted in to the "set as default" feature, try to get default payment method from elements session
if allowsSetAsDefaultPM {
if let customer = customer,
let defaultPaymentMethod = customer.getDefaultOrFirstPaymentMethod() {
defaultPaymentMethodOption = CustomerPaymentOption.stripeId(defaultPaymentMethod.stripeId)
}
}
else {
defaultPaymentMethodOption = CustomerPaymentOption.defaultPaymentMethod(for: customerID)
}

// Transform saved PaymentMethods into view models
let savedPMViewModels = savedPaymentMethods.compactMap { paymentMethod in
Expand All @@ -460,7 +476,7 @@ class SavedPaymentOptionsViewController: UIViewController {
let firstPaymentMethodIsLink = !showApplePay && showLink
let defaultIndex = firstPaymentMethodIsLink ? 2 : 1

let defaultSelectedIndex = viewModels.firstIndex(where: { $0 == defaultPaymentMethod }) ?? defaultIndex
let defaultSelectedIndex = viewModels.firstIndex(where: { $0 == defaultPaymentMethodOption }) ?? defaultIndex
return (defaultSelectedIndex, viewModels)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,13 @@ class PaymentSheetFlowControllerViewController: UIViewController, FlowController
isCVCRecollectionEnabled: false,
isTestMode: configuration.apiClient.isTestmode,
allowsRemovalOfLastSavedPaymentMethod: PaymentSheetViewController.allowsRemovalOfLastPaymentMethod(elementsSession: elementsSession, configuration: configuration),
allowsRemovalOfPaymentMethods: elementsSession.allowsRemovalOfPaymentMethodsForPaymentSheet()
allowsRemovalOfPaymentMethods: elementsSession.allowsRemovalOfPaymentMethodsForPaymentSheet(),
allowsSetAsDefaultPM: configuration.allowsSetAsDefaultPM
),
paymentSheetConfiguration: configuration,
intent: intent,
appearance: configuration.appearance,
elementsSession: elementsSession,
cbcEligible: elementsSession.isCardBrandChoiceEligible,
analyticsHelper: analyticsHelper
)
Expand Down
Loading

0 comments on commit 171edb3

Please sign in to comment.