Skip to content

Commit

Permalink
📱 Tap to Pay on iPhone!
Browse files Browse the repository at this point in the history
Co-authored-by: Sam Poder <[email protected]>
  • Loading branch information
YodaLightsabr and sampoder committed Sep 5, 2024
1 parent 249c0bf commit 9e52361
Show file tree
Hide file tree
Showing 11 changed files with 1,266 additions and 135 deletions.
13 changes: 13 additions & 0 deletions app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export default {
"applinks:hcb.hackclub.com",
"applinks:bank.hackclub.com",
],
entitlements: {
"com.apple.developer.proximity-reader.payment.acceptance": [],
// I'm not entirely sure what to do here
}
},
android: {
icon: "./assets/app-icon.png",
Expand Down Expand Up @@ -66,6 +70,15 @@ export default {
"expo-local-authentication",
{ faceIDPermission: "Allow $(PRODUCT_NAME) to use Face ID." },
],
[
"@stripe/stripe-terminal-react-native",
{
"bluetoothBackgroundMode": true,
"locationWhenInUsePermission": "Location access is required in order to accept payments.",
"bluetoothPeripheralPermission": "Bluetooth access is required in order to connect to supported bluetooth card readers.",
"bluetoothAlwaysUsagePermission": "This app uses Bluetooth to connect to supported card readers."
}
],
],
},
};
204 changes: 176 additions & 28 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
"@config-plugins/react-native-dynamic-app-icon": "^7.0.0",
"@expo/vector-icons": "^14.0.0",
"@react-native-async-storage/async-storage": "1.21.0",
"@react-native-community/geolocation": "^3.3.0",
"@react-native-menu/menu": "^0.9.1",
"@react-navigation/bottom-tabs": "^6.5.8",
"@react-navigation/native": "^6.1.10",
"@react-navigation/native-stack": "^6.9.13",
"@stripe/stripe-terminal-react-native": "^0.0.1-beta.20",
"date-fns": "^2.30.0",
"expo": "^50.0.8",
"expo-auth-session": "~5.4.0",
Expand All @@ -34,6 +36,8 @@
"expo-image-picker": "~14.7.1",
"expo-linear-gradient": "~12.7.2",
"expo-linking": "~6.2.2",
"expo-local-authentication": "~13.8.0",
"expo-location": "^17.0.1",
"expo-notifications": "~0.27.6",
"expo-secure-store": "~12.8.1",
"expo-splash-screen": "~0.26.4",
Expand All @@ -48,14 +52,14 @@
"react-native-dynamic-app-icon": "^1.1.0",
"react-native-gesture-handler": "~2.14.0",
"react-native-pager-view": "6.2.3",
"react-native-progress": "^5.0.1",
"react-native-reanimated": "~3.6.2",
"react-native-safe-area-context": "4.8.2",
"react-native-screens": "~3.29.0",
"react-native-svg": "14.1.0",
"react-native-web": "^0.19.8",
"swr": "^2.2.1",
"ts-pattern": "^5.0.8",
"expo-local-authentication": "~13.8.0"
"ts-pattern": "^5.0.8"
},
"devDependencies": {
"@babel/core": "^7.22.11",
Expand Down
15 changes: 15 additions & 0 deletions src/Navigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import RenameTransactionPage from "./pages/RenameTransaction";
import SettingsPage from "./pages/Settings";
import TransactionPage from "./pages/Transaction";
import { palette } from "./theme";
import OrganizationDonationPage from "./pages/organization/Donation";

Check failure on line 31 in src/Navigator.tsx

View workflow job for this annotation

GitHub Actions / ESLint

`./pages/organization/Donation` import should occur before import of `./pages/organization/Settings`
import ProcessDonationPage from "./pages/organization/ProcessDonation";

Check failure on line 32 in src/Navigator.tsx

View workflow job for this annotation

GitHub Actions / ESLint

`./pages/organization/ProcessDonation` import should occur before import of `./pages/organization/Settings`

const Stack = createNativeStackNavigator<StackParamList>();
const CardsStack = createNativeStackNavigator<CardsStackParamList>();
Expand Down Expand Up @@ -150,6 +152,19 @@ export default function Navigator() {
title: "Manage Organization",
}}
/>
<Stack.Screen
name="OrganizationDonation"
component={OrganizationDonationPage}
options={{
headerBackTitle: "Back",
title: "Collect Donations",
}}
/>
<Stack.Screen
name="ProcessDonation"
component={ProcessDonationPage}
options={{ presentation: "modal", title: "Process Donation" }}
/>
<Stack.Screen
options={{ headerBackTitle: "Back" }}
name="Transaction"
Expand Down
257 changes: 257 additions & 0 deletions src/components/Stripe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import { useEffect, useState } from 'react'
import {
Alert,
ImageBackground,

Check failure on line 4 in src/components/Stripe.tsx

View workflow job for this annotation

GitHub Actions / ESLint

'ImageBackground' is defined but never used
Linking,
SafeAreaView,
Text,
TextInput,
View,
} from 'react-native'
import { useStripeTerminal } from '@stripe/stripe-terminal-react-native'

Check failure on line 11 in src/components/Stripe.tsx

View workflow job for this annotation

GitHub Actions / ESLint

`@stripe/stripe-terminal-react-native` import should occur before import of `react`



import Button from "../components/Button";
import { useLocation } from '../lib/useLocation';

export default function Stripe() {
const { initialize, connectLocalMobileReader } = useStripeTerminal();

Check failure on line 19 in src/components/Stripe.tsx

View workflow job for this annotation

GitHub Actions / ESLint

'connectLocalMobileReader' is assigned a value but never used
useEffect(() => {
initialize();
}, [initialize]);
return (
<PageStripe />
);
}


interface PaymentIntent {
id: string
amount: number
created: string
currency: string
sdkUuid: string
paymentMethodId: string
}

export function PageStripe() {
const { location, accessDenied } = useLocation()

Check failure on line 39 in src/components/Stripe.tsx

View workflow job for this annotation

GitHub Actions / ESLint

'location' is assigned a value but never used

const [value, setValue] = useState(0)
const [reader, setReader] = useState()
const [payment, setPayment] = useState<PaymentIntent>()
const [loadingCreatePayment, setLoadingCreatePayment] = useState(false)
const [loadingCollectPayment, setLoadingCollectPayment] = useState(false)
const [loadingConfirmPayment, setLoadingConfirmPayment] = useState(false)
const [loadingConnectingReader, setLoadingConnectingReader] = useState(false)

const locationIdStripeMock = 'tml_FrcFgksbiIZZ2V'

const {
discoverReaders,
connectLocalMobileReader,
createPaymentIntent,
collectPaymentMethod,
confirmPaymentIntent,
connectedReader,
} = useStripeTerminal({
onUpdateDiscoveredReaders: (readers: any) => {

Check failure on line 59 in src/components/Stripe.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected any. Specify a different type
setReader(readers[0])
},
})

useEffect(() => {
discoverReaders({
discoveryMethod: 'localMobile',
simulated: true,
})
}, [discoverReaders])

async function connectReader(selectedReader: any) {

Check failure on line 71 in src/components/Stripe.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected any. Specify a different type
setLoadingConnectingReader(true)
try {
const { reader, error } = await connectLocalMobileReader({
reader: selectedReader,
locationId: locationIdStripeMock,
})

if (error) {
console.log('connectLocalMobileReader error:', error)

Check warning on line 80 in src/components/Stripe.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected console statement
return
}

Alert.alert('Reader connected successfully')

console.log('Reader connected successfully', reader)

Check warning on line 86 in src/components/Stripe.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected console statement
} catch (error) {
console.log(error)

Check warning on line 88 in src/components/Stripe.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected console statement
} finally {
setLoadingConnectingReader(false)
}
}

async function paymentIntent() {
setLoadingCreatePayment(true)
try {
const { error, paymentIntent } = await createPaymentIntent({
amount: Number((value * 100).toFixed()),
currency: 'usd',
paymentMethodTypes: ['card_present'],
offlineBehavior: 'prefer_online',
})

if (error) {
console.log('Error creating payment intent', error)

Check warning on line 105 in src/components/Stripe.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected console statement
return
}

setPayment(paymentIntent)

Alert.alert('Payment intent created successfully')
} catch (error) {
console.log(error)

Check warning on line 113 in src/components/Stripe.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected console statement
} finally {
setLoadingCreatePayment(false)
}
}

async function collectPayment() {
setLoadingCollectPayment(true)
try {
const { error, paymentIntent } = await collectPaymentMethod({
paymentIntent: payment,
} as any)

Check failure on line 124 in src/components/Stripe.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected any. Specify a different type

if (error) {
console.log('Error collecting payment', error)

Check warning on line 127 in src/components/Stripe.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected console statement
Alert.alert('Error collecting payment', error.message)
return
}

Alert.alert('Payment successfully collected', '', [
{
text: 'Ok',
onPress: async () => {
console.log(paymentIntent);

Check warning on line 136 in src/components/Stripe.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected console statement
await confirmPayment()
},
},
])
} catch (error) {
console.log(error)

Check warning on line 142 in src/components/Stripe.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected console statement
} finally {
setLoadingCollectPayment(false)
}
}

async function confirmPayment() {
setLoadingConfirmPayment(true)
console.log("foo", { payment })

Check warning on line 150 in src/components/Stripe.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected console statement
try {
const { error, paymentIntent } = await confirmPaymentIntent({
paymentIntent: payment as any

Check failure on line 153 in src/components/Stripe.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected any. Specify a different type
})

if (error) {
console.log('Error confirm payment', error)

Check warning on line 157 in src/components/Stripe.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected console statement
return
}

Alert.alert('Payment successfully confirmed!', 'Congratulations')
console.log('Payment confirmed', paymentIntent)
} catch (error) {
console.log(error)
} finally {
setLoadingConfirmPayment(false)
}
}

async function handleRequestLocation() {
await Linking.openSettings()
}

useEffect(() => {
if (accessDenied) {
Alert.alert(
'Access to location',
'To use the app, you need to allow the use of your device location.',
[
{
text: 'Activate',
onPress: handleRequestLocation,
},
]
)
}
}, [accessDenied])

return (
<SafeAreaView style={{ flex: 1 }}>
<View
style={{
justifyContent: 'space-between',
alignItems: 'center',
flex: 1,
}}
>
<View style={{ paddingTop: 10, gap: 10 }}>
<Text
style={{
fontSize: 20,
fontWeight: 'bold',
textAlign: 'center',
}}
>
Stripe
</Text>
</View>
<View style={{ gap: 10 }}>
<View style={{ marginBottom: 20, gap: 10 }}>
<Text
style={{
fontSize: 18,
fontWeight: 'bold',
color: '#635AFF',
}}
>
Amount to be charged
</Text>
<TextInput
style={{
borderColor: '#635AFF',
borderWidth: 1,
borderRadius: 8,
padding: 15,
}}
placeholder="Enter the value"
onChangeText={(inputValue) => setValue(Number(inputValue))}
keyboardType="numeric"
/>
</View>

<Button
onPress={() => connectReader(reader)}
loading={loadingConnectingReader}
>Connecting with the reader{'disabled' + !!connectedReader}</Button>

<Button
onPress={paymentIntent}
loading={loadingCreatePayment}
>Create payment intent{'disabled' + !connectedReader}</Button>

<Button
onPress={collectPayment}
loading={loadingCollectPayment}
>Collect payment{'disabled' + !connectedReader}</Button>

<Button
onPress={confirmPayment as any}
loading={loadingConfirmPayment}
>Confirm payment</Button>
</View>
<View></View>
</View>
</SafeAreaView>
)
}
2 changes: 2 additions & 0 deletions src/lib/NavigatorParamList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export type StackParamList = {
Invitation: { inviteId: Invitation["id"]; invitation?: Invitation };
Event: { orgId: Organization["id"]; organization?: Organization };
AccountNumber: { orgId: Organization["id"] };
ProcessDonation: { orgId: Organization["id"], payment: any };
OrganizationSettings: { orgId: Organization["id"] };
OrganizationDonation: { orgId: Organization["id"] };
Transaction: {
transactionId: Transaction["id"];
orgId?: Organization["id"];
Expand Down
Loading

0 comments on commit 9e52361

Please sign in to comment.