Skip to content

Commit

Permalink
feature: Update DX for Apple Pay (#236)
Browse files Browse the repository at this point in the history
* Update DX for Apple Pay

* Add configurable fields for ApplePay Express Checkout to enable testing

---------

Co-authored-by: Boris Nikolic <[email protected]>
  • Loading branch information
borisprimer and BorisNikolic authored Oct 4, 2024
1 parent 1974ddf commit 07031b1
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 89 deletions.
2 changes: 1 addition & 1 deletion packages/example/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ target 'example_0_70_6' do
pod 'PrimerSDK'
pod 'Primer3DS'
pod 'PrimerIPay88MYSDK'
pod 'PrimerKlarnaSDK', '1.1.0'
pod 'PrimerKlarnaSDK', '1.1.1'
pod 'PrimerStripeSDK', '1.0.0'

target 'example_0_70_6Tests' do
Expand Down
17 changes: 8 additions & 9 deletions packages/example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ PODS:
- React-Core
- Primer3DS (2.3.2)
- PrimerIPay88MYSDK (0.1.7)
- PrimerKlarnaSDK (1.1.0)
- PrimerKlarnaSDK (1.1.1)
- PrimerSDK (2.31.0):
- PrimerSDK/Core (= 2.31.0)
- PrimerSDK/Core (2.31.0)
Expand Down Expand Up @@ -291,7 +291,7 @@ PODS:
- React-jsinspector (0.72.0)
- React-logger (0.72.0):
- glog
- react-native-safe-area-context (4.10.1):
- react-native-safe-area-context (4.8.1):
- React-Core
- react-native-segmented-control (2.4.3):
- React-Core
Expand Down Expand Up @@ -400,10 +400,9 @@ PODS:
- React-jsi (= 0.72.0)
- React-logger (= 0.72.0)
- React-perflogger (= 0.72.0)
- RNScreens (3.31.1):
- RNScreens (3.29.0):
- RCT-Folly (= 2021.07.22.00)
- React-Core
- React-RCTImage
- SocketRocket (0.6.0)
- Yoga (1.14.0)

Expand All @@ -416,7 +415,7 @@ DEPENDENCIES:
- primer-io-react-native (from `../../sdk`)
- Primer3DS
- PrimerIPay88MYSDK
- PrimerKlarnaSDK (= 1.1.0)
- PrimerKlarnaSDK (= 1.1.1)
- PrimerSDK
- PrimerStripeSDK (= 1.0.0)
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
Expand Down Expand Up @@ -563,7 +562,7 @@ SPEC CHECKSUMS:
primer-io-react-native: 4b4eb3b62c7787537191a6f74598db8a78c29587
Primer3DS: 81e7969033230c7346a517cd609be956914738bb
PrimerIPay88MYSDK: 436ee0be7e2c97e4e81456ccddee20175e9e3c4d
PrimerKlarnaSDK: 83e9a1357a7247bf8fa2836fc945cf97644d601d
PrimerKlarnaSDK: 564105170cc7b467bf95c31851813ea41c468f8b
PrimerSDK: 78d1a48ba9b4677f78da9750448af6c72007f4fe
PrimerStripeSDK: c37d4e7c1b5256d67d4890c4cc4b38ddc9427489
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
Expand All @@ -581,7 +580,7 @@ SPEC CHECKSUMS:
React-jsiexecutor: de4e98ab0f2661affeb54931806a79a93a34d1ea
React-jsinspector: 8d754fc957255a29d93e52fc67a895045cdc8703
React-logger: 454ffb01980778a43b0153ee98721d0275b56616
react-native-safe-area-context: dcab599c527c2d7de2d76507a523d20a0b83823d
react-native-safe-area-context: cd1169d797a2ef722a00bfc5af10748d5b6c94f9
react-native-segmented-control: d88b35fcd06c0c8e1f1f7ed1eb066c2b02570fdd
React-NativeModulesApple: 1d81d927ef1a67a3545a01e14c2e98500bf9b199
React-perflogger: 684a11499a0589cc42135d6d5cc04d0e4e0e261a
Expand All @@ -600,10 +599,10 @@ SPEC CHECKSUMS:
React-runtimescheduler: 4a36521cc1ec1bc3997ae2462b6779dadaae376b
React-utils: c12d2e75c8bbc727939ddc4319ed95493395ed5a
ReactCommon: b9547f82aed45eccc1aa59034dc6c72809e37000
RNScreens: 93ae3be2f119d955620f9bbb39ad372adb53b7a9
RNScreens: 3c5b9f4a9dcde752466854b6109b79c0e205dad3
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
Yoga: 1d6727ed193122f6adaf435c3de1a768326ff83b

PODFILE CHECKSUM: e1d93bf98428be80f71d4652c0496e390624cf68
PODFILE CHECKSUM: 4505e915c8982e98dcb8297a1c627233ce3a067f

COCOAPODS: 1.15.2
132 changes: 66 additions & 66 deletions packages/example/ios/example_0_70_6.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

11 changes: 2 additions & 9 deletions packages/example/src/screens/CheckoutScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,8 @@ const CheckoutScreen = (props: any) => {
merchantName: appPaymentParameters.merchantName || "Merchant name",
isCaptureBillingAddressEnabled: true,
showApplePayForUnsupportedDevice: true,
checkProvidedNetworks: false
checkProvidedNetworks: false,
shippingOptions: appPaymentParameters.shippingOptions
},
googlePayOptions: {
isCaptureBillingAddressEnabled: true,
Expand Down Expand Up @@ -321,14 +322,6 @@ const CheckoutScreen = (props: any) => {
}
};

if (appPaymentParameters.merchantName) {
//@ts-ignore
settings.paymentMethodOptions.applePayOptions = {
merchantIdentifier: 'merchant.checkout.team',
merchantName: appPaymentParameters.merchantName
};
}

const onVaultManagerButtonTapped = async () => {
try {
setIsLoading(true);
Expand Down
103 changes: 103 additions & 0 deletions packages/example/src/screens/SettingsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { styles } from '../styles';
import SegmentedControl from '@react-native-segmented-control/segmented-control';
import { Environment, makeEnvironmentFromIntVal, makePaymentHandlingFromIntVal, PaymentHandling } from '../network/Environment';
import { appPaymentParameters, IClientSessionAddress, IClientSessionCustomer, IClientSessionLineItem, IClientSessionOrder, IClientSessionPaymentMethod, IClientSessionPaymentMethodOptions, IClientSessionRequestBody } from '../models/IClientSessionRequestBody';
import { useState } from 'react';
import { Switch } from 'react-native';
import TextField from '../components/TextField';
import type { NewLineItemScreenProps } from './NewLineItemSreen';
Expand All @@ -22,6 +23,11 @@ export interface AppPaymentParameters {
paymentHandling: PaymentHandling;
clientSessionRequestBody: IClientSessionRequestBody;
merchantName?: string;
shippingOptions?: {
isCaptureShippingAddressEnabled: boolean;
requireShippingMethod: boolean;
additionalShippingContactFields: string[];
};
}

export let customApiKey: string | undefined;
Expand Down Expand Up @@ -66,6 +72,11 @@ const SettingsScreen = ({ navigation }) => {
const [visaSurcharge, setVisaSurcharge] = React.useState<number | undefined>(appPaymentParameters.clientSessionRequestBody.paymentMethod?.options?.PAYMENT_CARD?.networks.VISA?.surcharge.amount);
const [masterCardSurcharge, setMasterCardSurcharge] = React.useState<number | undefined>(appPaymentParameters.clientSessionRequestBody.paymentMethod?.options?.PAYMENT_CARD?.networks.MASTERCARD?.surcharge.amount);

const [isShippingOptionsApplied, setIsShippingOptionsApplied] = useState<boolean>(false);
const [isCaptureShippingAddressEnabled, setIsCaptureShippingAddressEnabled] = useState<boolean>(false);
const [requireShippingMethod, setRequireShippingMethod] = useState<boolean>(false);
const [additionalShippingContactFields, setAdditionalShippingContactFields] = useState<string[]>([]);

const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.black : Colors.white
};
Expand Down Expand Up @@ -617,6 +628,7 @@ const SettingsScreen = ({ navigation }) => {
{renderMerchantSection()}
{renderCustomerSection()}
{renderSurchargeSection()}
{renderShippingOptionsSection()}
</View>
);
}
Expand Down Expand Up @@ -674,6 +686,91 @@ const SettingsScreen = ({ navigation }) => {
);
}

const renderShippingOptionsSection = () => {
return (
<View style={{ marginTop: 12, marginBottom: 8 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ ...styles.heading1, marginBottom: 4 }}>
Shipping Options
</Text>
<View style={{ flex: 1 }} />
<Switch
value={isShippingOptionsApplied}
onValueChange={val => {
setIsShippingOptionsApplied(val);
}}
/>
</View>

{
!isShippingOptionsApplied ? null :
<View>
<View style={{ flexDirection: 'row', alignItems: 'center', marginVertical: 4 }}>
<Text style={{ flex: 1 }}>Capture Shipping Address Enabled</Text>
<Switch
value={isCaptureShippingAddressEnabled}
onValueChange={val => {
setIsCaptureShippingAddressEnabled(val);
}}
/>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center', marginVertical: 4 }}>
<Text style={{ flex: 1 }}>Require Shipping Method</Text>
<Switch
value={requireShippingMethod}
onValueChange={val => {
setRequireShippingMethod(val);
}}
/>
</View>
<Text style={{ ...styles.heading2, marginVertical: 4 }}>Additional Shipping Contact Fields</Text>
<View style={{ marginVertical: 4 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ flex: 1 }}>Name</Text>
<Switch
value={additionalShippingContactFields.includes('name')}
onValueChange={val => {
if (val) {
setAdditionalShippingContactFields([...additionalShippingContactFields, 'name']);
} else {
setAdditionalShippingContactFields(additionalShippingContactFields.filter(field => field !== 'name'));
}
}}
/>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ flex: 1 }}>Email Address</Text>
<Switch
value={additionalShippingContactFields.includes('emailAddress')}
onValueChange={val => {
if (val) {
setAdditionalShippingContactFields([...additionalShippingContactFields, 'emailAddress']);
} else {
setAdditionalShippingContactFields(additionalShippingContactFields.filter(field => field !== 'emailAddress'));
}
}}
/>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ flex: 1 }}>Phone Number</Text>
<Switch
value={additionalShippingContactFields.includes('phoneNumber')}
onValueChange={val => {
if (val) {
setAdditionalShippingContactFields([...additionalShippingContactFields, 'phoneNumber']);
} else {
setAdditionalShippingContactFields(additionalShippingContactFields.filter(field => field !== 'phoneNumber'));
}
}}
/>
</View>
</View>
</View>
}
</View>
);
};

const updateAppPaymentParameters = () => {
appPaymentParameters.merchantName = merchantName;
appPaymentParameters.environment = environment;
Expand Down Expand Up @@ -779,6 +876,12 @@ const SettingsScreen = ({ navigation }) => {
currentClientSessionRequestBody.paymentMethod = Object.keys(currentPaymentMethod).length === 0 ? undefined : currentPaymentMethod;

appPaymentParameters.clientSessionRequestBody = currentClientSessionRequestBody;
appPaymentParameters.shippingOptions = isShippingOptionsApplied ? {
isCaptureShippingAddressEnabled: isCaptureShippingAddressEnabled,
requireShippingMethod: requireShippingMethod,
additionalShippingContactFields: additionalShippingContactFields,
} : undefined;

}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,51 @@ extension PrimerSettings {

var applePayOptions: PrimerApplePayOptions?
if let rnApplePayOptions = ((settingsJson["paymentMethodOptions"] as? [String: Any])?["applePayOptions"] as? [String: Any]),
let rnApplePayMerchantIdentifier = rnApplePayOptions["merchantIdentifier"] as? String,
let rnApplePayMerchantName = rnApplePayOptions["merchantName"] as? String
let rnApplePayMerchantIdentifier = rnApplePayOptions["merchantIdentifier"] as? String
{
let rnApplePayMerchantName = rnApplePayOptions["merchantName"] as? String
let rnApplePayIsCaptureBillingAddressEnabled = (rnApplePayOptions["isCaptureBillingAddressEnabled"] as? Bool) ?? false
let rnApplePayShowApplePayForUnsupportedDevice = (rnApplePayOptions["showApplePayForUnsupportedDevice"] as? Bool) ?? true
let rnApplePayCheckProvidedNetworks = (rnApplePayOptions["checkProvidedNetworks"] as? Bool) ?? true

var shippingOptions: PrimerApplePayOptions.ShippingOptions? = nil

Check failure on line 44 in packages/sdk/ios/Sources/DataModels/PrimerSettings+Extensions.swift

View workflow job for this annotation

GitHub Actions / Run iOS unit tests

'ShippingOptions' is not a member type of class 'PrimerSDK.PrimerApplePayOptions'

Check failure on line 44 in packages/sdk/ios/Sources/DataModels/PrimerSettings+Extensions.swift

View workflow job for this annotation

GitHub Actions / Run iOS unit tests

'ShippingOptions' is not a member type of class 'PrimerSDK.PrimerApplePayOptions'
if let rnShippingOptions = rnApplePayOptions["shippingOptions"] as? [String: Any] {
let isCaptureShippingAddressEnabled = rnShippingOptions["isCaptureShippingAddressEnabled"] as? Bool ?? false
let requireShippingMethod = rnShippingOptions["requireShippingMethod"] as? Bool ?? false

var additionalShippingContactFields: [PrimerApplePayOptions.ShippingOptions.AdditionalShippingContactField]? = nil

Check failure on line 49 in packages/sdk/ios/Sources/DataModels/PrimerSettings+Extensions.swift

View workflow job for this annotation

GitHub Actions / Run iOS unit tests

'ShippingOptions' is not a member type of class 'PrimerSDK.PrimerApplePayOptions'

Check failure on line 49 in packages/sdk/ios/Sources/DataModels/PrimerSettings+Extensions.swift

View workflow job for this annotation

GitHub Actions / Run iOS unit tests

'ShippingOptions' is not a member type of class 'PrimerSDK.PrimerApplePayOptions'
if let additionalFieldsStrings = rnShippingOptions["additionalShippingContactFields"] as? [String] {
additionalShippingContactFields = additionalFieldsStrings.compactMap { fieldStr in
switch fieldStr {
case "name":
return .name

Check failure on line 54 in packages/sdk/ios/Sources/DataModels/PrimerSettings+Extensions.swift

View workflow job for this annotation

GitHub Actions / Run iOS unit tests

cannot infer contextual base in reference to member 'name'

Check failure on line 54 in packages/sdk/ios/Sources/DataModels/PrimerSettings+Extensions.swift

View workflow job for this annotation

GitHub Actions / Run iOS unit tests

cannot infer contextual base in reference to member 'name'
case "emailAddress":
return .emailAddress
case "phoneNumber":
return .phoneNumber
default:
return nil // Ignore unknown values or handle accordingly
}
}
}

shippingOptions = PrimerApplePayOptions.ShippingOptions(

Check failure on line 65 in packages/sdk/ios/Sources/DataModels/PrimerSettings+Extensions.swift

View workflow job for this annotation

GitHub Actions / Run iOS unit tests

type 'PrimerApplePayOptions' has no member 'ShippingOptions'

Check failure on line 65 in packages/sdk/ios/Sources/DataModels/PrimerSettings+Extensions.swift

View workflow job for this annotation

GitHub Actions / Run iOS unit tests

type 'PrimerApplePayOptions' has no member 'ShippingOptions'
isCaptureShippingAddressEnabled: isCaptureShippingAddressEnabled,
additionalShippingContactFields: additionalShippingContactFields,
requireShippingMethod: requireShippingMethod
)
}

applePayOptions = PrimerApplePayOptions(
merchantIdentifier: rnApplePayMerchantIdentifier,
merchantName: rnApplePayMerchantName,
isCaptureBillingAddressEnabled: rnApplePayIsCaptureBillingAddressEnabled,
showApplePayForUnsupportedDevice: rnApplePayShowApplePayForUnsupportedDevice,
checkProvidedNetworks: rnApplePayCheckProvidedNetworks)
checkProvidedNetworks: rnApplePayCheckProvidedNetworks,
shippingOptions: shippingOptions

Check failure on line 78 in packages/sdk/ios/Sources/DataModels/PrimerSettings+Extensions.swift

View workflow job for this annotation

GitHub Actions / Run iOS unit tests

extra argument 'shippingOptions' in call

Check failure on line 78 in packages/sdk/ios/Sources/DataModels/PrimerSettings+Extensions.swift

View workflow job for this annotation

GitHub Actions / Run iOS unit tests

extra argument 'shippingOptions' in call
)
}


var klarnaOptions: PrimerKlarnaOptions?
if let rnKlarnaRecurringPaymentDescription = ((settingsJson["paymentMethodOptions"] as? [String: Any])?["klarnaOptions"] as? [String: Any])?["recurringPaymentDescription"] as? String {
Expand Down
21 changes: 20 additions & 1 deletion packages/sdk/src/models/PrimerSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,30 @@ interface IPrimerApayaOptions {

interface IPrimerApplePayOptions {
merchantIdentifier: string;
merchantName: string;
/**
* @deprecated Use Client Session API to provide merchant name value: https://primer.io/docs/payment-methods/apple-pay/direct-integration#prepare-the-client-session
*/
merchantName?: string;
isCaptureBillingAddressEnabled?: boolean;
/**
* If you don't want to present the Apple Pay option when the device doesn't support it, set this to `false`.
* Default value is `true`.
*/
showApplePayForUnsupportedDevice?: boolean;
/**
* Due to reports about the Apple Pay flow not presenting because `canMakePayments(usingNetworks:)` was returning false
* when there were no cards in the Wallet, we introduced this flag to continue supporting the old behavior.
* Default value is `true`.
*/
checkProvidedNetworks?: boolean;
shippingOptions?: IShippingOptions;
}
interface IShippingOptions {
isCaptureShippingAddressEnabled: boolean;
additionalShippingContactFields?: AdditionalShippingContactField[];
requireShippingMethod: boolean;
}
type AdditionalShippingContactField = 'name' | 'emailAddress' | 'phoneNumber';

interface IPrimerCardPaymentOptions {
is3DSOnVaultingEnabled: boolean;
Expand Down

0 comments on commit 07031b1

Please sign in to comment.