react-native-fpay 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Fpay.podspec +20 -0
- package/LICENSE +20 -0
- package/README.md +37 -0
- package/android/build.gradle +67 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/fpay/FpayModule.kt +15 -0
- package/android/src/main/java/com/fpay/FpayPackage.kt +31 -0
- package/ios/Fpay.h +5 -0
- package/ios/Fpay.mm +21 -0
- package/lib/module/FountainPayProvider.js +18 -0
- package/lib/module/FountainPayProvider.js.map +1 -0
- package/lib/module/core/api/client.js +47 -0
- package/lib/module/core/api/client.js.map +1 -0
- package/lib/module/core/api/index.js +35 -0
- package/lib/module/core/api/index.js.map +1 -0
- package/lib/module/core/types/index.js +4 -0
- package/lib/module/core/types/index.js.map +1 -0
- package/lib/module/engine/BLEReceiverService.js +190 -0
- package/lib/module/engine/BLEReceiverService.js.map +1 -0
- package/lib/module/engine/BLESenderService.js +259 -0
- package/lib/module/engine/BLESenderService.js.map +1 -0
- package/lib/module/engine/FPEngine.js +340 -0
- package/lib/module/engine/FPEngine.js.map +1 -0
- package/lib/module/engine/NearbyUsersService.js +87 -0
- package/lib/module/engine/NearbyUsersService.js.map +1 -0
- package/lib/module/index.js +16 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/ui/components/FPButton.js +71 -0
- package/lib/module/ui/components/FPButton.js.map +1 -0
- package/lib/module/ui/components/LoadingAnimation/InLoading.js +74 -0
- package/lib/module/ui/components/LoadingAnimation/InLoading.js.map +1 -0
- package/lib/module/ui/components/LoadingAnimation/index.js +82 -0
- package/lib/module/ui/components/LoadingAnimation/index.js.map +1 -0
- package/lib/module/ui/components/OtpInput/OTPInputView.js +290 -0
- package/lib/module/ui/components/OtpInput/OTPInputView.js.map +1 -0
- package/lib/module/ui/components/OtpInput/Styles.js +20 -0
- package/lib/module/ui/components/OtpInput/Styles.js.map +1 -0
- package/lib/module/ui/components/OtpInput/helpers/codeToArray.js +7 -0
- package/lib/module/ui/components/OtpInput/helpers/codeToArray.js.map +1 -0
- package/lib/module/ui/components/OtpInput/helpers/device.js +9 -0
- package/lib/module/ui/components/OtpInput/helpers/device.js.map +1 -0
- package/lib/module/ui/components/OtpInput/helpers/styles.js +17 -0
- package/lib/module/ui/components/OtpInput/helpers/styles.js.map +1 -0
- package/lib/module/ui/components/OtpInput/helpers/types.js +4 -0
- package/lib/module/ui/components/OtpInput/helpers/types.js.map +1 -0
- package/lib/module/ui/components/OtpInput/index.js +45 -0
- package/lib/module/ui/components/OtpInput/index.js.map +1 -0
- package/lib/module/ui/components/PulseAnimation.js +61 -0
- package/lib/module/ui/components/PulseAnimation.js.map +1 -0
- package/lib/module/ui/modals/FPPaymentRequestModal.js +253 -0
- package/lib/module/ui/modals/FPPaymentRequestModal.js.map +1 -0
- package/lib/module/ui/modals/FPShell.js +180 -0
- package/lib/module/ui/modals/FPShell.js.map +1 -0
- package/lib/module/ui/screens/ReceiveScreen.js +291 -0
- package/lib/module/ui/screens/ReceiveScreen.js.map +1 -0
- package/lib/module/ui/screens/SendScreen.js +216 -0
- package/lib/module/ui/screens/SendScreen.js.map +1 -0
- package/lib/module/ui/screens/sub/BluetoothSubScreen.js +403 -0
- package/lib/module/ui/screens/sub/BluetoothSubScreen.js.map +1 -0
- package/lib/module/ui/screens/sub/NFCSubScreen.js +169 -0
- package/lib/module/ui/screens/sub/NFCSubScreen.js.map +1 -0
- package/lib/module/ui/screens/sub/NQRSubScreen.js +136 -0
- package/lib/module/ui/screens/sub/NQRSubScreen.js.map +1 -0
- package/lib/module/ui/screens/sub/ProximitySubScreen.js +501 -0
- package/lib/module/ui/screens/sub/ProximitySubScreen.js.map +1 -0
- package/lib/module/ui/screens/sub/TransferSubScreen.js +361 -0
- package/lib/module/ui/screens/sub/TransferSubScreen.js.map +1 -0
- package/lib/module/ui/theme/index.js +64 -0
- package/lib/module/ui/theme/index.js.map +1 -0
- package/lib/module/useFountainPay.js +82 -0
- package/lib/module/useFountainPay.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/FountainPayProvider.d.ts +7 -0
- package/lib/typescript/src/FountainPayProvider.d.ts.map +1 -0
- package/lib/typescript/src/core/api/client.d.ts +7 -0
- package/lib/typescript/src/core/api/client.d.ts.map +1 -0
- package/lib/typescript/src/core/api/index.d.ts +67 -0
- package/lib/typescript/src/core/api/index.d.ts.map +1 -0
- package/lib/typescript/src/core/types/index.d.ts +130 -0
- package/lib/typescript/src/core/types/index.d.ts.map +1 -0
- package/lib/typescript/src/engine/BLEReceiverService.d.ts +43 -0
- package/lib/typescript/src/engine/BLEReceiverService.d.ts.map +1 -0
- package/lib/typescript/src/engine/BLESenderService.d.ts +39 -0
- package/lib/typescript/src/engine/BLESenderService.d.ts.map +1 -0
- package/lib/typescript/src/engine/FPEngine.d.ts +24 -0
- package/lib/typescript/src/engine/FPEngine.d.ts.map +1 -0
- package/lib/typescript/src/engine/NearbyUsersService.d.ts +19 -0
- package/lib/typescript/src/engine/NearbyUsersService.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/FPButton.d.ts +12 -0
- package/lib/typescript/src/ui/components/FPButton.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/LoadingAnimation/InLoading.d.ts +7 -0
- package/lib/typescript/src/ui/components/LoadingAnimation/InLoading.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/LoadingAnimation/index.d.ts +6 -0
- package/lib/typescript/src/ui/components/LoadingAnimation/index.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/OtpInput/OTPInputView.d.ts +29 -0
- package/lib/typescript/src/ui/components/OtpInput/OTPInputView.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/OtpInput/Styles.d.ts +330 -0
- package/lib/typescript/src/ui/components/OtpInput/Styles.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/OtpInput/helpers/codeToArray.d.ts +6 -0
- package/lib/typescript/src/ui/components/OtpInput/helpers/codeToArray.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/OtpInput/helpers/device.d.ts +6 -0
- package/lib/typescript/src/ui/components/OtpInput/helpers/device.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/OtpInput/helpers/styles.d.ts +6 -0
- package/lib/typescript/src/ui/components/OtpInput/helpers/styles.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/OtpInput/helpers/types.d.ts +84 -0
- package/lib/typescript/src/ui/components/OtpInput/helpers/types.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/OtpInput/index.d.ts +9 -0
- package/lib/typescript/src/ui/components/OtpInput/index.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/PulseAnimation.d.ts +2 -0
- package/lib/typescript/src/ui/components/PulseAnimation.d.ts.map +1 -0
- package/lib/typescript/src/ui/modals/FPPaymentRequestModal.d.ts +2 -0
- package/lib/typescript/src/ui/modals/FPPaymentRequestModal.d.ts.map +1 -0
- package/lib/typescript/src/ui/modals/FPShell.d.ts +2 -0
- package/lib/typescript/src/ui/modals/FPShell.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/ReceiveScreen.d.ts +10 -0
- package/lib/typescript/src/ui/screens/ReceiveScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/SendScreen.d.ts +9 -0
- package/lib/typescript/src/ui/screens/SendScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/sub/BluetoothSubScreen.d.ts +552 -0
- package/lib/typescript/src/ui/screens/sub/BluetoothSubScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/sub/NFCSubScreen.d.ts +19 -0
- package/lib/typescript/src/ui/screens/sub/NFCSubScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/sub/NQRSubScreen.d.ts +13 -0
- package/lib/typescript/src/ui/screens/sub/NQRSubScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/sub/ProximitySubScreen.d.ts +552 -0
- package/lib/typescript/src/ui/screens/sub/ProximitySubScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/sub/TransferSubScreen.d.ts +12 -0
- package/lib/typescript/src/ui/screens/sub/TransferSubScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/theme/index.d.ts +62 -0
- package/lib/typescript/src/ui/theme/index.d.ts.map +1 -0
- package/lib/typescript/src/useFountainPay.d.ts +3 -0
- package/lib/typescript/src/useFountainPay.d.ts.map +1 -0
- package/package.json +217 -0
- package/src/FountainPayProvider.tsx +21 -0
- package/src/core/api/client.ts +47 -0
- package/src/core/api/index.ts +61 -0
- package/src/core/types/index.ts +144 -0
- package/src/engine/BLEReceiverService.ts +244 -0
- package/src/engine/BLESenderService.ts +314 -0
- package/src/engine/FPEngine.ts +370 -0
- package/src/engine/NearbyUsersService.ts +106 -0
- package/src/index.ts +30 -0
- package/src/ui/components/FPButton.tsx +42 -0
- package/src/ui/components/LoadingAnimation/InLoading.tsx +88 -0
- package/src/ui/components/LoadingAnimation/index.tsx +93 -0
- package/src/ui/components/OtpInput/OTPInputView.tsx +243 -0
- package/src/ui/components/OtpInput/Styles.ts +19 -0
- package/src/ui/components/OtpInput/helpers/codeToArray.ts +3 -0
- package/src/ui/components/OtpInput/helpers/device.ts +6 -0
- package/src/ui/components/OtpInput/helpers/styles.ts +17 -0
- package/src/ui/components/OtpInput/helpers/types.ts +88 -0
- package/src/ui/components/OtpInput/index.tsx +51 -0
- package/src/ui/components/PulseAnimation.tsx +78 -0
- package/src/ui/modals/FPPaymentRequestModal.tsx +158 -0
- package/src/ui/modals/FPShell.tsx +107 -0
- package/src/ui/screens/ReceiveScreen.tsx +119 -0
- package/src/ui/screens/SendScreen.tsx +86 -0
- package/src/ui/screens/sub/BluetoothSubScreen.tsx +433 -0
- package/src/ui/screens/sub/NFCSubScreen.tsx +83 -0
- package/src/ui/screens/sub/NQRSubScreen.tsx +61 -0
- package/src/ui/screens/sub/ProximitySubScreen.tsx +390 -0
- package/src/ui/screens/sub/TransferSubScreen.tsx +146 -0
- package/src/ui/theme/index.ts +24 -0
- package/src/useFountainPay.ts +95 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
// FPay SDK — BluetoothSubScreen (Send side)
|
|
2
|
+
//
|
|
3
|
+
// Payment flow:
|
|
4
|
+
// 1. Scan via FPEngine.scanForBluetoothDevices (BLESenderService reads USER_INFO char)
|
|
5
|
+
// 2. User picks a device, taps "Send Payment Request"
|
|
6
|
+
// 3. FPEngine.sendBluetoothPaymentRequest writes JSON to receiver's REQUEST char
|
|
7
|
+
// 4. Receiver's FPPaymentRequestModal pops up — they tap Accept or Decline
|
|
8
|
+
// 5. Accept → receiver writes { accepted, accountDetails, requestTimestamp } to RESPONSE char
|
|
9
|
+
// Decline → receiver writes { accepted: false, timestamp }
|
|
10
|
+
// 6. Sender polls RESPONSE char every 1s (BLESenderService.sendPaymentRequest)
|
|
11
|
+
// and matches by requestTimestamp === request.timestamp
|
|
12
|
+
// 7. On accept: sender calls transferAPI.send() with the returned accountDetails
|
|
13
|
+
|
|
14
|
+
import React, { useEffect, useState } from 'react';
|
|
15
|
+
import { View, Text, TouchableOpacity, Alert, StyleSheet } from 'react-native';
|
|
16
|
+
import styled from 'styled-components/native';
|
|
17
|
+
import Svg, { Path } from 'react-native-svg';
|
|
18
|
+
import Ionicons from 'react-native-vector-icons/Ionicons';
|
|
19
|
+
|
|
20
|
+
import { transferAPI } from '../../../core/api';
|
|
21
|
+
import { FPButton } from '../../components/FPButton';
|
|
22
|
+
import { C, R, S, F, shadow } from '../../theme';
|
|
23
|
+
import type { FPCurrency, FPError, FPTransaction, FintechDevice } from '../../../core/types';
|
|
24
|
+
|
|
25
|
+
import { FPEngine } from '../../../engine/FPEngine';
|
|
26
|
+
|
|
27
|
+
import { PulseAnimation } from '../../components/PulseAnimation';
|
|
28
|
+
import InLoading from '../../components/LoadingAnimation/InLoading';
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
const Container = styled(View)`
|
|
32
|
+
flex: 1;
|
|
33
|
+
background-color: #000000;
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const Header = styled(View)`
|
|
37
|
+
flex-direction: row;
|
|
38
|
+
align-items: center;
|
|
39
|
+
justify-content: space-between;
|
|
40
|
+
padding: 12px 16px;
|
|
41
|
+
position: absolute;
|
|
42
|
+
top: 40px;
|
|
43
|
+
left: 0;
|
|
44
|
+
right: 0;
|
|
45
|
+
z-index: 9999999999;
|
|
46
|
+
elevation: 4;
|
|
47
|
+
shadow-color: #000;
|
|
48
|
+
shadow-offset: 0px 2px;
|
|
49
|
+
shadow-opacity: 0.1;
|
|
50
|
+
shadow-radius: 4px;
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const HeaderButton = styled(TouchableOpacity)`
|
|
54
|
+
padding: 8px;
|
|
55
|
+
background-color: #fff;
|
|
56
|
+
width: 40px;
|
|
57
|
+
height: 40px;
|
|
58
|
+
border-radius: 50px;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
align-items: center;
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
const HeaderCenter = styled(View)`
|
|
64
|
+
flex-direction: row;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 8px;
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
const HeaderRight = styled(View)`
|
|
70
|
+
flex-direction: row;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: 8px;
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
const QuickLinksContainer = styled(View)`
|
|
76
|
+
align-items: center;
|
|
77
|
+
margin-top: 250px;
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
const QuickLinksContainerWithDevices = styled(View)`
|
|
81
|
+
align-items: flex-start;
|
|
82
|
+
margin-top: 30%;
|
|
83
|
+
margin-bottom: 20px;
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
const QuickLinksIcon = styled(TouchableOpacity)`
|
|
87
|
+
width: 96px;
|
|
88
|
+
height: 96px;
|
|
89
|
+
border-radius: 48px;
|
|
90
|
+
background-color: #18181b;
|
|
91
|
+
justify-content: center;
|
|
92
|
+
align-items: center;
|
|
93
|
+
margin-bottom: 16px;
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
const QuickLinksText = styled(Text)`
|
|
97
|
+
color: #9ca3af;
|
|
98
|
+
font-size: 18px;
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
export const DeviceCard = styled(View)`
|
|
102
|
+
background-color: white;
|
|
103
|
+
border-radius: 12px;
|
|
104
|
+
padding: 16px;
|
|
105
|
+
margin-bottom: 12px;
|
|
106
|
+
flex-direction: row;
|
|
107
|
+
justify-content: space-between;
|
|
108
|
+
align-items: center;
|
|
109
|
+
shadow-color: #000;
|
|
110
|
+
shadow-offset: 0px 2px;
|
|
111
|
+
shadow-opacity: 0.1;
|
|
112
|
+
shadow-radius: 8px;
|
|
113
|
+
elevation: 3;
|
|
114
|
+
`;
|
|
115
|
+
|
|
116
|
+
export const DeviceInfo = styled(View)`
|
|
117
|
+
flex: 1;
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
export const DeviceName = styled(Text)`
|
|
121
|
+
font-size: 16px;
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
color: #111827;
|
|
124
|
+
margin-bottom: 4px;
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
export const DeviceSignal = styled(Text)`
|
|
128
|
+
font-size: 14px;
|
|
129
|
+
color: #6b7280;
|
|
130
|
+
`;
|
|
131
|
+
|
|
132
|
+
export const PayButton = styled(TouchableOpacity)`
|
|
133
|
+
background-color: #2563eb;
|
|
134
|
+
padding: 10px 20px;
|
|
135
|
+
border-radius: 8px;
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
export const PayButtonText = styled(Text)`
|
|
139
|
+
color: white;
|
|
140
|
+
font-size: 14px;
|
|
141
|
+
font-weight: 600;
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
const ScrollContainer = styled.ScrollView`
|
|
145
|
+
flex: 1;
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
const ContentContainer = styled(View)`
|
|
149
|
+
padding: 14px;
|
|
150
|
+
margin-bottom: 80%;
|
|
151
|
+
`;
|
|
152
|
+
|
|
153
|
+
const BluetoothIconContainer = styled(View)`
|
|
154
|
+
align-items: center;
|
|
155
|
+
margin-top: 15%;
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
// ── SVG Icons — your originals, unchanged ────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
const BluetoothIcon = ({ color = '#111', size = 22 }) => (
|
|
161
|
+
<Svg width={size} height={size} strokeWidth="0.9" viewBox="0 0 24 24" fill="none">
|
|
162
|
+
<Path d="M6 19.0007C3.57111 17.1763 2 14.2716 2 11C2 5.47715 6.47715 1 12 1C17.5228 1 22 5.47715 22 11C22 14.2716 20.4289 17.1763 18 19.0007" stroke="#fff" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
|
|
163
|
+
<Path d="M6 19.0007C3.57111 17.1763 2 14.2716 2 11C2 5.47715 6.47715 1 12 1C17.5228 1 22 5.47715 22 11C22 14.2716 20.4289 17.1763 18 19.0007" stroke="#fff" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
|
|
164
|
+
<Path d="M7.52779 15C6.57771 13.9385 6 12.5367 6 11C6 7.68629 8.68629 5 12 5C15.3137 5 18 7.68629 18 11C18 12.5367 17.4223 13.9385 16.4722 15" stroke="#fff" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
|
|
165
|
+
<Path fillRule="evenodd" clipRule="evenodd" d="M9.25 11C9.25 9.48122 10.4812 8.25 12 8.25C13.5188 8.25 14.75 9.48122 14.75 11C14.75 12.5188 13.5188 13.75 12 13.75C10.4812 13.75 9.25 12.5188 9.25 11Z" fill="#fff" />
|
|
166
|
+
<Path d="M15.0776 21.4865C14.8566 22.8126 13.7093 23.7844 12.365 23.7844H11.7536C10.4093 23.7844 9.262 22.8126 9.041 21.4865L8.53213 18.4333C8.29232 16.9946 9.43086 15.8854 10.5339 15.15C11.9123 14.231 12.3864 14.3406 13.5847 15.1396C14.6421 15.8445 15.8263 16.9946 15.5865 18.4333L15.0776 21.4865Z" fill="#fff" />
|
|
167
|
+
</Svg>
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const BluetoothScanningIcon = ({ color = '#111', size = 22 }) => (
|
|
171
|
+
<Svg width={size} height={size} strokeWidth="0.9" viewBox="0 0 24 24" fill="none">
|
|
172
|
+
<Path d="M6.75 8L17.25 16.5L11.75 22V2L17.25 7.5L6.75 16" stroke={color} strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
|
|
173
|
+
</Svg>
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
type Phase =
|
|
178
|
+
| 'idle' // nothing happening
|
|
179
|
+
| 'scanning' // BLE scan running
|
|
180
|
+
| 'selected' // device chosen, ready to send
|
|
181
|
+
| 'requesting' // request sent, waiting for receiver
|
|
182
|
+
| 'transferring' // BLE accepted, calling transfer API
|
|
183
|
+
| 'done'
|
|
184
|
+
| 'declined'
|
|
185
|
+
| 'error';
|
|
186
|
+
|
|
187
|
+
const PHASE_LABEL: Record<Phase, string> = {
|
|
188
|
+
idle: 'Ready',
|
|
189
|
+
scanning: 'Scanning for nearby FPay devices...',
|
|
190
|
+
selected: 'Device selected — ready to send',
|
|
191
|
+
requesting: 'Waiting for receiver to respond...',
|
|
192
|
+
transferring: 'Processing transfer...',
|
|
193
|
+
done: 'Payment sent!',
|
|
194
|
+
declined: 'Request was declined',
|
|
195
|
+
error: 'Something went wrong',
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
interface Props {
|
|
199
|
+
mode: 'send' | 'receive';
|
|
200
|
+
amount: number;
|
|
201
|
+
currency: FPCurrency;
|
|
202
|
+
onBack: () => void;
|
|
203
|
+
onDone: () => void;
|
|
204
|
+
onSuccess?: (tx: FPTransaction) => void;
|
|
205
|
+
onError?: (err: FPError) => void;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function BluetoothSubScreen({
|
|
209
|
+
mode, amount, currency,
|
|
210
|
+
onBack, onDone, onSuccess, onError,
|
|
211
|
+
}: Props) {
|
|
212
|
+
const [devices, setDevices] = useState<FintechDevice[]>([]);
|
|
213
|
+
const [scanning, setScanning] = useState(false);
|
|
214
|
+
const [status, setStatus] = useState('Ready to scan');
|
|
215
|
+
const [currentDevice, setCurrentDevice] = useState<FintechDevice | null>(null);
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
// ── 1. Scan ───────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
const handleScan = async () => {
|
|
222
|
+
setDevices([]);
|
|
223
|
+
setScanning(true);
|
|
224
|
+
setStatus('Scanning for nearby users...');
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
await FPEngine.scanForBluetoothDevices(
|
|
228
|
+
(device) => {
|
|
229
|
+
setDevices((prevDevices) => {
|
|
230
|
+
// Avoid duplicates
|
|
231
|
+
const exists = prevDevices.find(d => d.id === device.id);
|
|
232
|
+
if (exists) return prevDevices;
|
|
233
|
+
return [...prevDevices, device];
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
5000 // Scan for 5 seconds
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
setScanning(false);
|
|
240
|
+
setStatus(`Found ${devices.length} user(s) nearby`);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.error('Scan error:', error);
|
|
243
|
+
setScanning(false);
|
|
244
|
+
setStatus('Scan failed');
|
|
245
|
+
Alert.alert('Error', 'Failed to scan for devices');
|
|
246
|
+
}finally{
|
|
247
|
+
setScanning(false);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const handlePayment = async (device: FintechDevice) => {
|
|
252
|
+
// Show payment amount input dialog (implement your own)
|
|
253
|
+
Alert.alert(
|
|
254
|
+
'Send Payment',
|
|
255
|
+
`Send payment to ${device.userName || device.name}?`,
|
|
256
|
+
[
|
|
257
|
+
{
|
|
258
|
+
text: 'Cancel',
|
|
259
|
+
style: 'cancel',
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
text: `Continue`,
|
|
263
|
+
onPress: () => initiatePayment(device, 50),
|
|
264
|
+
},
|
|
265
|
+
]
|
|
266
|
+
);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
const initiatePayment = async (device: FintechDevice, amount: number) => {
|
|
271
|
+
try {
|
|
272
|
+
setCurrentDevice(device);
|
|
273
|
+
// Connect to the device
|
|
274
|
+
const user = FPEngine.getUser();
|
|
275
|
+
await FPEngine.deviceConnection(device.id);
|
|
276
|
+
|
|
277
|
+
// Create payment request
|
|
278
|
+
const request: any = {
|
|
279
|
+
amount,
|
|
280
|
+
currency,
|
|
281
|
+
senderId: user?.userId ?? user?.accountNumber ?? 'unknown',
|
|
282
|
+
senderName: user?.accountName ?? 'FountainPay User',
|
|
283
|
+
timestamp: Date.now(),
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
setStatus(`Sending payment request...`);
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
// Send the payment request
|
|
290
|
+
console.log("Payment Request: ", request)
|
|
291
|
+
const response: any = await await FPEngine.sendBluetoothPaymentRequest(device.id, request);
|
|
292
|
+
|
|
293
|
+
console.log("Component response: ", response)
|
|
294
|
+
|
|
295
|
+
if (!response.accepted) {
|
|
296
|
+
Alert.alert(
|
|
297
|
+
'Payment Declined',
|
|
298
|
+
`${device.userName} declined the payment`,
|
|
299
|
+
[{ text: 'OK' }]
|
|
300
|
+
);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!response.accountDetails) {
|
|
305
|
+
throw {
|
|
306
|
+
code: 'BT_MISSING_ACCOUNT',
|
|
307
|
+
message: 'Receiver accepted but sent no account details',
|
|
308
|
+
} as FPError;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const result = await transferAPI.send({
|
|
312
|
+
accountNumber: response.accountDetails.accountNumber,
|
|
313
|
+
bankCode: response.accountDetails.bankCode,
|
|
314
|
+
amount: amount * 100,
|
|
315
|
+
narration: `Bluetooth payment to ${response.accountDetails.accountName}`,
|
|
316
|
+
reference: response.transactionId,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const tx: FPTransaction = {
|
|
320
|
+
id: result.reference,
|
|
321
|
+
reference: result.reference,
|
|
322
|
+
type: 'debit',
|
|
323
|
+
channel: 'bluetooth',
|
|
324
|
+
amount: result.amount,
|
|
325
|
+
currency: result.currency,
|
|
326
|
+
status: result.status,
|
|
327
|
+
recipient: { accountName: response.accountDetails.accountName },
|
|
328
|
+
createdAt: result.createdAt,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
onSuccess?.(tx);
|
|
332
|
+
setCurrentDevice(null);
|
|
333
|
+
setStatus('Waiting for response...');
|
|
334
|
+
} catch (error: any) {
|
|
335
|
+
const fp = error as FPError;
|
|
336
|
+
setCurrentDevice(null);
|
|
337
|
+
if (error.message.includes('timeout')) {
|
|
338
|
+
Alert.alert(
|
|
339
|
+
'Request Timeout',
|
|
340
|
+
'The receiver did not respond in time. They may not have seen the request.',
|
|
341
|
+
[{ text: 'OK' }]
|
|
342
|
+
);
|
|
343
|
+
} else {
|
|
344
|
+
Alert.alert('Error', 'Failed to send payment request');
|
|
345
|
+
}
|
|
346
|
+
onError?.(fp);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<Container>
|
|
353
|
+
{/* Status Bar */}
|
|
354
|
+
|
|
355
|
+
<Header>
|
|
356
|
+
<HeaderCenter>
|
|
357
|
+
<Text style={{ fontWeight: 'bold', fontSize: 16, color:'#FFF' }}>Bluetooth</Text>
|
|
358
|
+
<Text style={{ fontSize: 10, color: '#FFF' }}>Transfer</Text>
|
|
359
|
+
</HeaderCenter>
|
|
360
|
+
|
|
361
|
+
<HeaderRight>
|
|
362
|
+
|
|
363
|
+
<HeaderButton>
|
|
364
|
+
<Ionicons name="ellipsis-vertical" size={22} />
|
|
365
|
+
</HeaderButton>
|
|
366
|
+
</HeaderRight>
|
|
367
|
+
</Header>
|
|
368
|
+
|
|
369
|
+
{!scanning && !devices.length && (
|
|
370
|
+
<QuickLinksContainer>
|
|
371
|
+
<QuickLinksIcon onPress={handleScan}>
|
|
372
|
+
<BluetoothIcon size={40} color="#FFF" />
|
|
373
|
+
</QuickLinksIcon>
|
|
374
|
+
<QuickLinksText>Tap to Pay</QuickLinksText>
|
|
375
|
+
<Text style={{ fontSize: 16, color: '#999' }}>
|
|
376
|
+
No nearby users found
|
|
377
|
+
</Text>
|
|
378
|
+
</QuickLinksContainer>
|
|
379
|
+
)}
|
|
380
|
+
|
|
381
|
+
{scanning && (
|
|
382
|
+
<>
|
|
383
|
+
|
|
384
|
+
<QuickLinksContainer>
|
|
385
|
+
<PulseAnimation />
|
|
386
|
+
<BluetoothIconContainer>
|
|
387
|
+
<BluetoothScanningIcon size={40} color="#FFF" />
|
|
388
|
+
<QuickLinksText>Scanning...</QuickLinksText>
|
|
389
|
+
</BluetoothIconContainer>
|
|
390
|
+
|
|
391
|
+
</QuickLinksContainer>
|
|
392
|
+
</>
|
|
393
|
+
|
|
394
|
+
)}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
{!scanning && devices.length > 0 && (
|
|
398
|
+
<ScrollContainer showsVerticalScrollIndicator={false}>
|
|
399
|
+
<ContentContainer>
|
|
400
|
+
<QuickLinksContainerWithDevices>
|
|
401
|
+
<QuickLinksText>Found {devices.length} user(s)</QuickLinksText>
|
|
402
|
+
</QuickLinksContainerWithDevices>
|
|
403
|
+
{devices.map(device => (
|
|
404
|
+
<DeviceCard key={device.id}>
|
|
405
|
+
<DeviceInfo>
|
|
406
|
+
<DeviceName>
|
|
407
|
+
{device.appName}: {device.userName || device.name}
|
|
408
|
+
|
|
409
|
+
</DeviceName>
|
|
410
|
+
<DeviceSignal>
|
|
411
|
+
Signal: {device.rssi} dBm
|
|
412
|
+
</DeviceSignal>
|
|
413
|
+
</DeviceInfo>
|
|
414
|
+
{currentDevice?.id === device.id ? (
|
|
415
|
+
<InLoading size={40} text="" />
|
|
416
|
+
) : (
|
|
417
|
+
<PayButton
|
|
418
|
+
onPress={() => handlePayment(device)}
|
|
419
|
+
activeOpacity={0.8}
|
|
420
|
+
>
|
|
421
|
+
<PayButtonText>Pay</PayButtonText>
|
|
422
|
+
</PayButton>
|
|
423
|
+
)}
|
|
424
|
+
</DeviceCard>
|
|
425
|
+
))}
|
|
426
|
+
</ContentContainer>
|
|
427
|
+
|
|
428
|
+
</ScrollContainer>
|
|
429
|
+
|
|
430
|
+
)}
|
|
431
|
+
</Container>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
|
|
3
|
+
import NfcManager, { NfcTech, Ndef } from 'react-native-nfc-manager';
|
|
4
|
+
import { transferAPI } from '../../../core/api';
|
|
5
|
+
import { FPButton } from '../../components/FPButton';
|
|
6
|
+
import { C, R, S, F } from '../../theme';
|
|
7
|
+
import type { FPCurrency, FPError, FPTransaction } from '../../../core/types';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
mode: 'send' | 'receive';
|
|
11
|
+
amount?: number; currency?: FPCurrency;
|
|
12
|
+
myWallet?: { accountName: string; accountNumber: string; bankCode: string; bankName: string };
|
|
13
|
+
onBack: () => void; onDone: () => void;
|
|
14
|
+
onSuccess?: (tx: FPTransaction) => void; onError?: (err: FPError) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type Phase = 'idle' | 'active' | 'success' | 'error';
|
|
18
|
+
|
|
19
|
+
export function NFCSubScreen({ mode, amount, currency = 'NGN', myWallet, onBack, onDone, onSuccess, onError }: Props) {
|
|
20
|
+
const [phase, setPhase] = useState<Phase>('idle');
|
|
21
|
+
const [msg, setMsg] = useState('');
|
|
22
|
+
|
|
23
|
+
const tapToReceive = async () => {
|
|
24
|
+
if (!myWallet) return;
|
|
25
|
+
setPhase('active');
|
|
26
|
+
try {
|
|
27
|
+
await NfcManager.start();
|
|
28
|
+
await NfcManager.requestTechnology(NfcTech.Ndef);
|
|
29
|
+
const bytes = Ndef.encodeMessage([Ndef.textRecord(JSON.stringify(myWallet))]);
|
|
30
|
+
if (bytes) await NfcManager.ndefHandler.writeNdefMessage(bytes);
|
|
31
|
+
setPhase('success'); setMsg('Your payment details are now on the NFC tag.');
|
|
32
|
+
} catch (e: any) { setPhase('error'); setMsg(e.message); onError?.({ code: 'NFC_ERROR', message: e.message }); }
|
|
33
|
+
finally { NfcManager.cancelTechnologyRequest(); }
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const tapToSend = async () => {
|
|
37
|
+
setPhase('active');
|
|
38
|
+
try {
|
|
39
|
+
await NfcManager.start();
|
|
40
|
+
await NfcManager.requestTechnology(NfcTech.Ndef);
|
|
41
|
+
const tag = await NfcManager.getTag();
|
|
42
|
+
if (!tag?.ndefMessage?.[0]?.payload) throw new Error('No wallet data on tag');
|
|
43
|
+
const payload = new Uint8Array(tag.ndefMessage[0].payload);
|
|
44
|
+
const text = Ndef.text.decodePayload(payload);
|
|
45
|
+
const wallet = JSON.parse(text);
|
|
46
|
+
const result = await transferAPI.send({ accountNumber: wallet.accountNumber, bankCode: wallet.bankCode, amount: (amount ?? 0) * 100, narration: 'NFC payment' });
|
|
47
|
+
onSuccess?.({ id: result.reference, reference: result.reference, type: 'debit', channel: 'nfc', amount: result.amount, currency: result.currency, status: result.status, recipient: { accountName: wallet.accountName }, createdAt: result.createdAt });
|
|
48
|
+
setPhase('success'); setMsg('Payment sent!');
|
|
49
|
+
} catch (e: any) { setPhase('error'); setMsg(e.message); onError?.({ code: 'NFC_ERROR', message: e.message }); }
|
|
50
|
+
finally { NfcManager.cancelTechnologyRequest(); }
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<View style={st.wrap}>
|
|
55
|
+
<Text onPress={onBack} style={st.back}>Back</Text>
|
|
56
|
+
<Text style={st.title}>NFC {mode === 'send' ? 'Payment' : 'Receive'}</Text>
|
|
57
|
+
<View style={[st.tapZone, phase === 'active' && st.tapActive, phase === 'success' && st.tapSuccess, phase === 'error' && st.tapError]}>
|
|
58
|
+
<Text style={st.tapEmoji}>{phase === 'success' ? 'OK' : phase === 'error' ? 'ERR' : 'NFC'}</Text>
|
|
59
|
+
<Text style={st.tapLabel}>
|
|
60
|
+
{phase === 'idle' ? (mode === 'send' ? 'Tap to pay' : 'Tap to receive')
|
|
61
|
+
: phase === 'active' ? 'Scanning...'
|
|
62
|
+
: phase === 'success' ? 'Success!'
|
|
63
|
+
: msg}
|
|
64
|
+
</Text>
|
|
65
|
+
</View>
|
|
66
|
+
{phase === 'idle' && <FPButton label={mode === 'send' ? 'Start NFC Scan' : 'Write to NFC Tag'} onPress={mode === 'send' ? tapToSend : tapToReceive} style={{ marginTop: S.md }} />}
|
|
67
|
+
{phase === 'success' && <FPButton label="Done" onPress={onDone} style={{ marginTop: S.md }} />}
|
|
68
|
+
{phase === 'error' && <FPButton label="Try Again" onPress={() => setPhase('idle')} style={{ marginTop: S.md }} />}
|
|
69
|
+
</View>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const st = StyleSheet.create({
|
|
74
|
+
wrap: { flex: 1, padding: S.lg },
|
|
75
|
+
back: { color: C.brand, fontSize: F.md, fontWeight: '600', marginBottom: S.md },
|
|
76
|
+
title: { fontSize: F.xl, fontWeight: '800', color: C.ink, marginBottom: S.lg },
|
|
77
|
+
tapZone: { height: 180, backgroundColor: C.ink, borderRadius: R.xl, justifyContent: 'center', alignItems: 'center' },
|
|
78
|
+
tapActive: { backgroundColor: C.brand },
|
|
79
|
+
tapSuccess: { backgroundColor: C.green },
|
|
80
|
+
tapError: { backgroundColor: C.red },
|
|
81
|
+
tapEmoji: { fontSize: 40, color: C.white, fontWeight: '800', marginBottom: S.sm },
|
|
82
|
+
tapLabel: { color: C.white, fontSize: F.lg, fontWeight: '700' },
|
|
83
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { View, Text, StyleSheet, ActivityIndicator, Image } from 'react-native';
|
|
3
|
+
import { nqrAPI } from '../../../core/api';
|
|
4
|
+
import { FPButton } from '../../components/FPButton';
|
|
5
|
+
import { C, R, S, F, shadow } from '../../theme';
|
|
6
|
+
import type { FPCurrency, FPError } from '../../../core/types';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
mode: 'send' | 'receive';
|
|
10
|
+
amount?: number; currency?: FPCurrency;
|
|
11
|
+
onBack: () => void; onDone: () => void;
|
|
12
|
+
onSuccess?: (tx: any) => void; onError?: (err: FPError) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function NQRSubScreen({ mode, amount, currency = 'NGN', onBack, onDone, onError }: Props) {
|
|
16
|
+
const [qr, setQr] = useState<{ qrCodeImageBase64: string; reference: string; expiresAt?: string } | null>(null);
|
|
17
|
+
const [loading, setLoading] = useState(false);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (mode === 'receive') {
|
|
21
|
+
setLoading(true);
|
|
22
|
+
nqrAPI.generate({ amount: amount ? amount * 100 : undefined, currency })
|
|
23
|
+
.then(setQr).catch(e => onError?.(e)).finally(() => setLoading(false));
|
|
24
|
+
}
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<View style={st.wrap}>
|
|
29
|
+
<Text onPress={onBack} style={st.back}>Back</Text>
|
|
30
|
+
<Text style={st.title}>{mode === 'receive' ? 'Your QR Code' : 'Scan QR Code'}</Text>
|
|
31
|
+
{loading && <ActivityIndicator size="large" color={C.brand} style={{ marginTop: S.xl }} />}
|
|
32
|
+
{qr && (
|
|
33
|
+
<View style={st.qrCard}>
|
|
34
|
+
<Image source={{ uri: 'data:image/png;base64,' + qr.qrCodeImageBase64 }} style={{ width: 200, height: 200 }} />
|
|
35
|
+
{amount && <Text style={st.amount}>{currency} {amount.toLocaleString('en-NG', { minimumFractionDigits: 2 })}</Text>}
|
|
36
|
+
<Text style={st.ref}>Ref: {qr.reference}</Text>
|
|
37
|
+
{qr.expiresAt && <Text style={st.expiry}>Expires: {new Date(qr.expiresAt).toLocaleTimeString()}</Text>}
|
|
38
|
+
</View>
|
|
39
|
+
)}
|
|
40
|
+
{mode === 'send' && (
|
|
41
|
+
<View style={st.placeholder}>
|
|
42
|
+
<Text style={st.placeholderText}>Camera scanner</Text>
|
|
43
|
+
<Text style={st.placeholderSub}>Integrate your preferred camera/QR library here and pass the scanned qrString to nqrAPI.pay()</Text>
|
|
44
|
+
</View>
|
|
45
|
+
)}
|
|
46
|
+
</View>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const st = StyleSheet.create({
|
|
51
|
+
wrap: { flex: 1, padding: S.lg },
|
|
52
|
+
back: { color: C.brand, fontSize: F.md, fontWeight: '600', marginBottom: S.md },
|
|
53
|
+
title: { fontSize: F.xl, fontWeight: '800', color: C.ink, marginBottom: S.lg },
|
|
54
|
+
qrCard: { alignItems: 'center', backgroundColor: C.white, borderRadius: R.xl, padding: S.xl, ...shadow.lg },
|
|
55
|
+
amount: { fontSize: F.xxl, fontWeight: '800', color: C.ink, marginTop: S.md },
|
|
56
|
+
ref: { fontSize: F.sm, color: C.muted, marginTop: S.sm },
|
|
57
|
+
expiry: { fontSize: F.xs, color: C.amber, marginTop: 4 },
|
|
58
|
+
placeholder: { flex: 1, backgroundColor: C.ink, borderRadius: R.xl, justifyContent: 'center', alignItems: 'center', padding: S.xl },
|
|
59
|
+
placeholderText: { color: C.white, fontSize: F.xl, fontWeight: '700' },
|
|
60
|
+
placeholderSub: { color: C.ghost, fontSize: F.sm, textAlign: 'center', marginTop: S.sm },
|
|
61
|
+
});
|