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

feat: ACH public API, sample UI and Android wrapper #216

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
099d71f
chore: implement StripeAchUserDetailsCollectableDataRN
FlaviuExtPrimer May 17, 2024
d486ed1
chore: implement StripeAchUserDetailsStep
FlaviuExtPrimer May 17, 2024
5b321ad
chore: implement PrimerRNHeadlessUniversalCheckoutStripeAchUserDetail…
FlaviuExtPrimer May 17, 2024
fbc8576
chore: implement StripeAchMandateManager
FlaviuExtPrimer May 17, 2024
36cbd16
chore: refactor StripeAchMandateManager
FlaviuExtPrimer May 20, 2024
53355c9
chore: convert TokenizationStartedRN into object
FlaviuExtPrimer May 20, 2024
57a9e88
chore: align ACH step and collectable naming with Android SDK
FlaviuExtPrimer May 23, 2024
5f5e3a8
chore: implement callback support for AchAdditionalInfo
FlaviuExtPrimer May 23, 2024
ebf027d
chore: implement AchManager
FlaviuExtPrimer May 23, 2024
c18dad8
chore: fix properties not being serialized
FlaviuExtPrimer May 23, 2024
cc4dd65
chore: implement vault manager support for ACH
FlaviuExtPrimer May 24, 2024
1456823
chore: implement HeadlessCheckoutAchScreen
FlaviuExtPrimer May 24, 2024
7704a10
chore: fix build issues caused by migration to compileSdk 34
FlaviuExtPrimer May 28, 2024
d5e6986
chore: remove react-native-picker/picker
FlaviuExtPrimer May 28, 2024
c2a0996
chore: update yarn.lock
FlaviuExtPrimer May 28, 2024
681c06c
chore: rename ACH component providing function to be generic
FlaviuExtPrimer May 28, 2024
10e5e11
chore: use generic name for ACH mandate manager
FlaviuExtPrimer May 28, 2024
39d5da7
chore: fix ACH imports
FlaviuExtPrimer May 28, 2024
d58492a
chore: fix ACH onValidationError handling
FlaviuExtPrimer May 28, 2024
c63b019
Merge remote-tracking branch 'origin/master' into feat/ACC-3019-ACC-3…
FlaviuExtPrimer May 28, 2024
b10316e
chore: upgrade Github Actions JDK to 17
FlaviuExtPrimer May 28, 2024
c54f881
chore: upgrade Github Action JDK for unit tests to 17
FlaviuExtPrimer May 28, 2024
da9f14a
chore: use lowercase string type for ACH steps and collectable data
FlaviuExtPrimer May 28, 2024
89907ff
chore: provide generic names for ACH steps and validatable data
FlaviuExtPrimer May 28, 2024
f500a22
chore: have AchManager take in paymentMethodType
FlaviuExtPrimer May 29, 2024
b014281
chore: rename StripeAchComponent to StripeAchUserDetailsComponent
FlaviuExtPrimer May 30, 2024
37fe91e
chore: updated IPrimerStripeOptions to use the same API for both plat…
FlaviuExtPrimer Jun 10, 2024
ff731d4
Merge branch 'master' of https://github.com/primer-io/primer-sdk-reac…
FlaviuExtPrimer Jun 13, 2024
c2ded20
Merge branch 'master' of https://github.com/primer-io/primer-sdk-reac…
FlaviuExtPrimer Jul 23, 2024
c262c34
chore: update Stripe wrapper, include deviceInfo and scenario in clie…
FlaviuExtPrimer Jul 23, 2024
6dd2000
chore: remove staging repo
FlaviuExtPrimer Aug 2, 2024
982fe3e
chore: add new lines
FlaviuExtPrimer Aug 2, 2024
8b02032
chore: remove eclipse prefs
FlaviuExtPrimer Aug 2, 2024
02ba97d
chore: bump RN target SDK to 34
FlaviuExtPrimer Aug 2, 2024
a697906
chore: refactor toWritableMap(), toWritableArray() for reuse
FlaviuExtPrimer Aug 2, 2024
475e7bf
chore: refactor error mapping code
FlaviuExtPrimer Aug 2, 2024
bdd2d50
chore: implement removeType()
FlaviuExtPrimer Aug 2, 2024
175b4d8
chore: add userAgent to client session
FlaviuExtPrimer Aug 8, 2024
75aa5f5
chore: rename AchManager user detail setter functions
FlaviuExtPrimer Aug 8, 2024
525451d
Merge remote-tracking branch 'origin/master' into feat/ACC-3019-ACC-3…
jnewc Sep 12, 2024
b05200a
Remove key
jnewc Sep 12, 2024
6131cbd
update android sdk to latest
jnewc Sep 12, 2024
c31f934
Align gradle version
jnewc Sep 12, 2024
688f1d9
Android updates for 2.30.1
jnewc Sep 13, 2024
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
1 change: 1 addition & 0 deletions packages/example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ dependencies {
implementation "io.primer:3ds-android:1.4.3"
implementation "io.primer:ipay88-my-android:1.0.3"
implementation "io.primer:klarna-android:1.0.4"
implementation "io.primer:stripe-android:1.0.0"

implementation project(path: ':primerioreactnative')

Expand Down
2 changes: 2 additions & 0 deletions packages/example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import RawRetailOutletScreen from './screens/RawRetailOutletScreen';
import HeadlessCheckoutVaultScreen from './screens/HeadlessCheckoutVaultScreen';
import HeadlessCheckoutKlarnaScreen from './screens/HeadlessCheckoutKlarnaScreen';
import HeadlessCheckoutWithRedirect from './screens/HeadlessCheckoutWithRedirect';
import HeadlessCheckoutStripeAchScreen from './screens/HeadlessCheckoutStripeAchScreen';
import { LogBox } from 'react-native';

const Stack = createNativeStackNavigator();
Expand All @@ -37,6 +38,7 @@ const App = () => {
<Stack.Screen name="RawRetailOutlet" component={RawRetailOutletScreen} />
<Stack.Screen name="Klarna" component={HeadlessCheckoutKlarnaScreen} />
<Stack.Screen name="HeadlessCheckoutWithRedirect" component={HeadlessCheckoutWithRedirect} />
<Stack.Screen name="HeadlessCheckoutStripeAchScreen" component={HeadlessCheckoutStripeAchScreen} />
</Stack.Navigator>
</NavigationContainer>
);
Expand Down
18 changes: 18 additions & 0 deletions packages/example/src/models/IClientSessionRequestBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface IClientSessionRequestBody {
customerId?: string;
orderId?: string;
currencyCode?: string;
metadata?: IClientSessionMetadata;
order?: IClientSessionOrder;
customer?: IClientSessionCustomer;
paymentMethod?: IClientSessionPaymentMethod;
Expand All @@ -17,6 +18,16 @@ export interface IClientSessionOrder {
lineItems?: Array<IClientSessionLineItem>
}

export interface IClientSessionMetadata {
scenario?: string;
deviceInfo?: IClientSessionDeviceInfo;
}

export interface IClientSessionDeviceInfo {
ipAddress?: string;
userAgent?: string;
}

export interface IClientSessionCustomer {
emailAddress?: string;
mobileNumber?: string;
Expand Down Expand Up @@ -91,6 +102,13 @@ export let appPaymentParameters: AppPaymentParameters = {
customerId: `rn-customer-${makeRandomString(8)}`,
orderId: `rn-order-${makeRandomString(8)}`,
currencyCode: 'EUR',
metadata: {
scenario: 'STRIPE_ACH_ONEOFF',
deviceInfo : {
ipAddress: '127.0.0.1',
userAgent: 'React Native'
}
},
order: {
countryCode: 'DE',
lineItems: [
Expand Down
17 changes: 17 additions & 0 deletions packages/example/src/screens/AchMandateAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { AchMandateManager } from '@primer-io/react-native';
import { Alert } from 'react-native';

export async function showAchMandateAlert() {
const achMandateManager = new AchMandateManager();
Alert.alert('ACH Mandate', 'Would you like to accept this mandate?', [
{
text: 'No',
onPress: () => achMandateManager.declineMandate(),
style: 'cancel',
},
{
text: 'Yes',
onPress: () => achMandateManager.acceptMandate()
},
]);
}
15 changes: 13 additions & 2 deletions packages/example/src/screens/HeadlessCheckoutScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ import {
NativeUIManager,
PaymentMethod,
PrimerSettings,
SessionIntent,
SessionIntent
} from '@primer-io/react-native';
import SegmentedControl from '@react-native-segmented-control/segmented-control';
import { showAchMandateAlert } from './AchMandateAlert';

let log: string = '';
let merchantPaymentId: string | null = null;
Expand Down Expand Up @@ -80,6 +81,9 @@ export const HeadlessCheckoutScreen = (props: any) => {
iOS: {
urlScheme: 'merchant://primer.io',
},
stripeOptions: {
publishableKey: "<PUT_PUBLISHABE_KEY_HERE>"
},
gt-prime marked this conversation as resolved.
Show resolved Hide resolved
},
debugOptions: {
is3DSSanityCheckEnabled: false
Expand Down Expand Up @@ -143,6 +147,11 @@ export const HeadlessCheckoutScreen = (props: any) => {
)}\n`,
);
setIsLoading(false);
switch(merchantCheckoutAdditionalInfo.additionalInfoName) {
case "DisplayStripeAchMandateAdditionalInfo":
showAchMandateAlert();
break;
}
},
onCheckoutComplete: checkoutData => {
merchantCheckoutData = checkoutData;
Expand Down Expand Up @@ -376,6 +385,8 @@ export const HeadlessCheckoutScreen = (props: any) => {
console.log("Payment session intent is " + selectedSessionIntent)
if (paymentMethod.paymentMethodType === "KLARNA") {
props.navigation.navigate('Klarna', { paymentSessionIntent: selectedSessionIntent });
} else if (paymentMethod.paymentMethodType === "STRIPE_ACH") {
props.navigation.navigate('HeadlessCheckoutStripeAchScreen');
} else {
await nativeUIManager.showPaymentMethod(SessionIntent.CHECKOUT);
}
Expand Down Expand Up @@ -530,4 +541,4 @@ export const HeadlessCheckoutScreen = (props: any) => {
{renderLoadingOverlay()}
</View>
);
};
};
199 changes: 199 additions & 0 deletions packages/example/src/screens/HeadlessCheckoutStripeAchScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import React, { useEffect, useState } from 'react';
import {
Text,
View,
Button,
StyleSheet,
} from 'react-native';
import {
AchManager,
StripeAchUserDetailsComponent,
AchStep,
PrimerError,
PrimerInvalidComponentData,
PrimerValidComponentData,
PrimerValidatingComponentData,
PrimerComponentDataValidationError,
AchManagerProps,
AchValidatableData,
} from '@primer-io/react-native';
import TextField from '../components/TextField';

const achManager = new AchManager();
let component: StripeAchUserDetailsComponent;

const HeadlessCheckoutStripeAchScreen = (props: any) => {
const [firstName, setFirstName] = useState<string>("");
const [lastName, setLastName] = useState<string>("");
const [emailAddress, setEmailAddress] = useState<string>("");
const [firstNameError, setFirstNameError] = useState<string | null>(null);
const [lastNameError, setLastNameError] = useState<string | null>(null);
const [emailAddressError, setEmailAddressError] = useState<string | null>(null);

useEffect(() => {
(async () => {
const achManagerProps: AchManagerProps = {
paymentMethodType: "STRIPE_ACH",
onStep: (data: AchStep) => {
const log = `\nonStep: ${JSON.stringify(data)}\n`;
console.log(log);
switch (data.stepName) {
case "userDetailsRetrieved":
setFirstName(data.firstName);
setLastName(data.lastName);
setEmailAddress(data.emailAddress);
break;

case "userDetailsCollected":
props.navigation.goBack();
break;
}
},
onError: (error: PrimerError) => {
const log = `\nonError: ${JSON.stringify(error)}\n`;
console.log(log);
},
onInvalid: (data: PrimerInvalidComponentData<AchValidatableData>) => {
const log = `\nonInvalid: ${JSON.stringify(data)}\n`;
console.log(log);
let error = data?.errors[0]?.description ?? null
switch (data.data.validatableDataName) {
case "firstName":
setFirstNameError(error);
break;
case "lastName":
setLastNameError(error);
break;
case "emailAddress":
setEmailAddressError(error);
break;
}
},
onValid: (data: PrimerValidComponentData<AchValidatableData>) => {
const log = `\nonValid: ${JSON.stringify(data)}\n`;
console.log(log);
switch (data.data.validatableDataName) {
case "firstName":
setFirstNameError(null);
break;
case "lastName":
setLastNameError(null);
break;
case "emailAddress":
setEmailAddressError(null);
break;
}
},
onValidating: (data: PrimerValidatingComponentData<AchValidatableData>) => {
const log = `\onValidating: ${JSON.stringify(data)}\n`;
console.log(log);
},
onValidationError: (data: PrimerComponentDataValidationError<AchValidatableData>) => {
const log = `\nonValidationError: ${JSON.stringify(data)}\n`;
console.log(log);
switch (data.data.validatableDataName) {
case "firstName":
setFirstNameError(data.error.description);
break;
case "lastName":
setLastNameError(data.error.description);
break;
case "emailAddress":
setEmailAddressError(data.error.description);
break;
}
},
};
console.log("Initializing Stripe ACH component")
try {
component = await achManager.provide(achManagerProps);
console.log("Starting Stripe ACH component");
component?.start();
} catch (error) {
console.log("Failed to init Stripe ACH component: " + error);
}
})()
}, []);

const onSubmit = async () => {
try {
await component.submit();
} catch (err) {
console.error(err);
}
};

const handleFirstNameChange = async (value: string) => {
console.log("Setting first name");
setFirstName(value);
await component.handleFirstNameChange(value);
};

const handleLastNameChange = async (value: string) => {
console.log("Setting last name");
setLastName(value);
await component.handleLastNameChange(value);
};

const handleEmailAddressChange = async (value: string) => {
console.log("Setting email address");
setEmailAddress(value);
await component.handleEmailAddressChange(value);
};

return (
<View
style={{
padding: 16,
flex: 1,
backgroundColor: 'white',
}}>

<Text style={{ fontSize: 18, fontWeight: 'bold', paddingBottom: 8 }}>Stripe ACH session</Text>

<TextFieldWithError title='First name' value={firstName} error={firstNameError} onChangeText={handleFirstNameChange} />
<TextFieldWithError title='Last name' value={lastName} error={lastNameError} onChangeText={handleLastNameChange} />
<TextFieldWithError title='Email address' value={emailAddress} error={emailAddressError} onChangeText={handleEmailAddressChange} />

<View style={styles.button}>
<Button
disabled={firstNameError != null || lastNameError != null || emailAddressError != null}
onPress={() => onSubmit()}
title="Submit"
/>
</View>
</View>
);
};

const TextFieldWithError = ({ title, value, error, onChangeText }: { title: string, value: string, error: string | null, onChangeText: (value: string) => void }) => {
return (
<View>
<TextField
title={title}
textInputStyle={{
backgroundColor: '#f5f5f5',
borderColor: '#f5f5f5',
paddingHorizontal: 10,
}}
style={{
marginTop: 20,
borderRadius: 5,
marginHorizontal: 5,
}}
placeholder={title}
value={value}
onChangeText={onChangeText}
/>
{error ? <Text style={{color: 'red', marginTop: 4, marginHorizontal: 10 }}>{error}</Text> : null}
</View>
);
}

const styles = StyleSheet.create({
button: {
paddingTop: 16,
},
});

export default HeadlessCheckoutStripeAchScreen;
27 changes: 23 additions & 4 deletions packages/example/src/screens/HeadlessCheckoutVaultScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import {
Alert,
Button,
FlatList,
ScrollView,
Expand All @@ -22,6 +23,7 @@ import {
PrimerSettings,
ValidationError
} from '@primer-io/react-native';
import { showAchMandateAlert } from './AchMandateAlert';

let log: string = "";
let merchantPaymentId: string | null = null;
Expand Down Expand Up @@ -84,6 +86,11 @@ export default HeadlessCheckoutVaultScreen = (props: any) => {
merchantCheckoutAdditionalInfo = additionalInfo;
updateLogs(`\nℹ️ onCheckoutPending\nadditionalInfo: ${JSON.stringify(additionalInfo, null, 2)}\n`);
setIsLoading(false);
switch(merchantCheckoutAdditionalInfo.additionalInfoName) {
case "DisplayStripeAchMandateAdditionalInfo":
showAchMandateAlert();
break;
}
},
onCheckoutPending: (checkoutAdditionalInfo) => {
merchantCheckoutAdditionalInfo = checkoutAdditionalInfo;
Expand Down Expand Up @@ -297,19 +304,31 @@ export default HeadlessCheckoutVaultScreen = (props: any) => {
switch (paymentMethodType) {
case "PAYMENT_CARD":
case "GOOGLE_PAY":
case "APPLE_PAY":
const last4Digits = item.paymentInstrumentData.last4Digits
case "APPLE_PAY": {
let last4Digits = item.paymentInstrumentData.last4Digits
if (last4Digits !== undefined) {
suffix = "••••" + last4Digits
}
break;
case "PAYPAL":
}
case "PAYPAL": {
suffix = item.paymentInstrumentData?.externalPayerInfo?.email ?? ""
break;
case "KLARNA":
}
case "KLARNA": {
const billingAddress = item.paymentInstrumentData?.sessionData?.billingAddress
suffix = billingAddress?.email ?? ""
break;
}
case "STRIPE_ACH": {
const bankName = item.paymentInstrumentData?.bankName ?? "-";
suffix = "(" + bankName + ")"
const last4Digits = item.paymentInstrumentData?.last4Digits;
if (last4Digits !== undefined) {
suffix += " ••••" + last4Digits
}
break;
}
}
return paymentMethodType + ": " + (suffix ?? "-");
}
Expand Down
Loading