Skip to content

Commit e011528

Browse files
committed
Merge branch 'master' of github.com:stripe/stripe-react-native into porter/checkout-embedded-init
2 parents f8f724e + eab3b07 commit e011528

17 files changed

Lines changed: 2939 additions & 5 deletions

MIGRATING.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
11
# Migration Guide
22

3+
## Android SDK 36 requirement (stripe-android 23.x)
4+
5+
Recent versions of `@stripe/stripe-react-native` depend on `stripe-android 23.x`, which requires:
6+
7+
- `compileSdkVersion` 36
8+
- `targetSdkVersion` 36
9+
- `minSdkVersion` 23
10+
11+
Update your app's `android/build.gradle`:
12+
13+
```groovy
14+
android {
15+
compileSdkVersion 36
16+
17+
defaultConfig {
18+
minSdkVersion 23
19+
targetSdkVersion 36
20+
}
21+
}
22+
```
23+
24+
If you cannot upgrade to Android SDK 36, pin to an older `@stripe/stripe-react-native` version that uses `stripe-android` 22.x or earlier.
25+
26+
This is an Android-specific build requirement and does not affect iOS.
27+
328
## Migrating from versions < 0.29.0
429

530
The legacy Apple Pay and Google Pay APIs (`useApplePay`, `useGooglePay`, `presentApplePay`, `confirmApplePayPayment`, `initGooglePay`, `presentGooglePay`, `createGooglePayPaymentMethod`, `<ApplePayButton />`, `<GooglePayButton />`) were removed in v0.29.0.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ to your `app.json` file, where `merchantIdentifier` is the Apple merchant ID obt
7575

7676
#### Android
7777

78-
- Android 5.0 (API level 21) and above
79-
- Your `compileSdkVersion` must be `34`. See [this issue](https://github.com/stripe/stripe-react-native/issues/812) for potential workarounds.
78+
- Android 6.0 (API level 23) and above
79+
- Your `compileSdkVersion` must be `36` or higher.
8080
- Android gradle plugin 4.x and above
8181
- Kotlin 2.x and above. See [this issue](https://github.com/stripe/stripe-react-native/issues/1924#issuecomment-2867227374) for how to update the Kotlin version when using react-native 0.77 and below or Expo SDK 52.
8282

android/gradle.properties

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
StripeSdk_kotlinVersion=2.2.21
2-
StripeSdk_compileSdkVersion=30
3-
StripeSdk_targetSdkVersion=28
4-
StripeSdk_minSdkVersion=21
2+
StripeSdk_compileSdkVersion=36
3+
StripeSdk_targetSdkVersion=36
4+
StripeSdk_minSdkVersion=23
55
# Keep StripeSdk_stripeVersion in sync with https://github.com/stripe/stripe-identity-react-native/blob/main/android/gradle.properties
66
StripeSdk_stripeVersion=23.6.+

example/src/App.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import BancontactPaymentScreen from './screens/BancontactPaymentScreen';
1818
import BancontactSetupFuturePaymentScreen from './screens/BancontactSetupFuturePaymentScreen';
1919
import BilliePaymentScreen from './screens/BilliePaymentScreen';
2020
import CashAppScreen from './screens/CashAppScreen';
21+
import CheckoutScreen from './screens/CheckoutScreen';
2122
import CollectBankAccountScreen from './screens/CollectBankAccountScreen';
2223
import ConnectAccountOnboardingScreen from './screens/ConnectAccountOnboardingScreen';
2324
import ConnectPaymentsListScreen from './screens/ConnectPaymentsListScreen';
@@ -98,6 +99,7 @@ export type RootStackParamList = {
9899
ACHSetupScreen: undefined;
99100
PayPalScreen: undefined;
100101
CashAppScreen: undefined;
102+
CheckoutScreen: undefined;
101103
AffirmScreen: undefined;
102104
CollectBankAccountScreen: undefined;
103105
PaymentSheetDeferredIntentScreen: undefined;
@@ -287,6 +289,11 @@ export default function App() {
287289
<Stack.Screen name="ACHSetupScreen" component={ACHSetupScreen} />
288290
<Stack.Screen name="PayPalScreen" component={PayPalScreen} />
289291
<Stack.Screen name="CashAppScreen" component={CashAppScreen} />
292+
<Stack.Screen
293+
name="CheckoutScreen"
294+
component={CheckoutScreen}
295+
options={{ title: 'Checkout Playground' }}
296+
/>
290297
<Stack.Screen name="AffirmScreen" component={AffirmScreen} />
291298
<Stack.Screen
292299
name="CollectBankAccountScreen"

example/src/api/checkoutSession.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import {
2+
hostedCheckoutEndpoint,
3+
normalizePaymentMethodTypes,
4+
shouldShowAutomaticTax,
5+
supportsAdvancedCollection,
6+
type CheckoutPlaygroundConfig,
7+
} from '../checkoutPlayground/types';
8+
9+
export type CheckoutSessionResponse = {
10+
publishableKey: string;
11+
checkoutSessionClientSecret: string;
12+
};
13+
14+
const isRecord = (value: unknown): value is Record<string, unknown> =>
15+
typeof value === 'object' && value !== null;
16+
17+
const isCheckoutSessionResponse = (
18+
value: unknown
19+
): value is CheckoutSessionResponse =>
20+
isRecord(value) &&
21+
typeof value.publishableKey === 'string' &&
22+
value.publishableKey.length > 0 &&
23+
typeof value.checkoutSessionClientSecret === 'string' &&
24+
value.checkoutSessionClientSecret.length > 0;
25+
26+
const getCheckoutSessionErrorMessage = (value: unknown): string | undefined => {
27+
if (typeof value === 'string') {
28+
const trimmedValue = value.trim();
29+
return trimmedValue || undefined;
30+
}
31+
32+
if (isRecord(value) && typeof value.error === 'string') {
33+
const trimmedError = value.error.trim();
34+
return trimmedError || undefined;
35+
}
36+
37+
return undefined;
38+
};
39+
40+
async function parseCheckoutSessionResponseBody(response: {
41+
text(): Promise<string>;
42+
}): Promise<unknown> {
43+
const responseText = await response.text();
44+
45+
if (!responseText.trim()) {
46+
return null;
47+
}
48+
49+
try {
50+
return JSON.parse(responseText) as unknown;
51+
} catch {
52+
return responseText;
53+
}
54+
}
55+
56+
export type CheckoutSessionRequestBody = {
57+
merchant_country_code: string;
58+
mode: CheckoutPlaygroundConfig['mode'];
59+
currency: CheckoutPlaygroundConfig['currency'];
60+
customer: CheckoutPlaygroundConfig['customerType'];
61+
allow_promotion_codes: boolean;
62+
phone_number_collection: boolean;
63+
shipping_address_collection: boolean;
64+
billing_address_collection: boolean;
65+
include_shipping_options: boolean;
66+
automatic_tax: boolean;
67+
payment_method_types: string[];
68+
adaptive_pricing: boolean;
69+
checkout_session_payment_method_save: 'enabled' | 'disabled';
70+
checkout_session_payment_method_remove: 'enabled' | 'disabled';
71+
customer_email?: string;
72+
};
73+
74+
export function buildCheckoutSessionRequestBody(
75+
config: CheckoutPlaygroundConfig
76+
): CheckoutSessionRequestBody {
77+
const advancedCollectionEnabled = supportsAdvancedCollection(config);
78+
const allowPromotionCodes = advancedCollectionEnabled
79+
? config.allowPromotionCodes
80+
: false;
81+
const phoneNumberCollection = advancedCollectionEnabled
82+
? config.phoneNumberCollection
83+
: false;
84+
const automaticTax = shouldShowAutomaticTax(config)
85+
? config.automaticTax
86+
: false;
87+
88+
// The hosted demo backend uses this synthetic country code to exercise tax flows.
89+
const body: CheckoutSessionRequestBody = {
90+
merchant_country_code: 'us_tax',
91+
mode: config.mode,
92+
currency: config.currency,
93+
customer: config.customerType,
94+
allow_promotion_codes: allowPromotionCodes,
95+
phone_number_collection: phoneNumberCollection,
96+
shipping_address_collection: config.shippingAddressCollection,
97+
billing_address_collection: config.billingAddressCollection,
98+
include_shipping_options: config.enableShipping,
99+
automatic_tax: automaticTax,
100+
payment_method_types: normalizePaymentMethodTypes(
101+
config.paymentMethodTypes
102+
),
103+
adaptive_pricing: config.adaptivePricing,
104+
checkout_session_payment_method_save:
105+
config.checkoutSessionPaymentMethodSave ? 'enabled' : 'disabled',
106+
checkout_session_payment_method_remove:
107+
config.checkoutSessionPaymentMethodRemove ? 'enabled' : 'disabled',
108+
};
109+
110+
if (config.adaptivePricing && config.adaptivePricingCountry !== 'none') {
111+
body.customer_email = `test+location_${config.adaptivePricingCountry.toUpperCase()}@example.com`;
112+
}
113+
114+
return body;
115+
}
116+
117+
export async function fetchCheckoutSessionParams(
118+
config: CheckoutPlaygroundConfig
119+
): Promise<CheckoutSessionResponse> {
120+
if (normalizePaymentMethodTypes(config.paymentMethodTypes).length === 0) {
121+
throw new Error('Select at least one payment method.');
122+
}
123+
124+
const response = await fetch(hostedCheckoutEndpoint, {
125+
method: 'POST',
126+
headers: {
127+
'Content-Type': 'application/json',
128+
},
129+
body: JSON.stringify(buildCheckoutSessionRequestBody(config)),
130+
});
131+
132+
const responseBody = await parseCheckoutSessionResponseBody(response);
133+
134+
if (!response.ok) {
135+
throw new Error(
136+
getCheckoutSessionErrorMessage(responseBody) ||
137+
'Failed to create checkout session.'
138+
);
139+
}
140+
141+
if (!isCheckoutSessionResponse(responseBody)) {
142+
throw new Error('Checkout session response was missing required fields.');
143+
}
144+
145+
return {
146+
publishableKey: responseBody.publishableKey,
147+
checkoutSessionClientSecret: responseBody.checkoutSessionClientSecret,
148+
};
149+
}

0 commit comments

Comments
 (0)