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.
Files changed (167) hide show
  1. package/Fpay.podspec +20 -0
  2. package/LICENSE +20 -0
  3. package/README.md +37 -0
  4. package/android/build.gradle +67 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/fpay/FpayModule.kt +15 -0
  7. package/android/src/main/java/com/fpay/FpayPackage.kt +31 -0
  8. package/ios/Fpay.h +5 -0
  9. package/ios/Fpay.mm +21 -0
  10. package/lib/module/FountainPayProvider.js +18 -0
  11. package/lib/module/FountainPayProvider.js.map +1 -0
  12. package/lib/module/core/api/client.js +47 -0
  13. package/lib/module/core/api/client.js.map +1 -0
  14. package/lib/module/core/api/index.js +35 -0
  15. package/lib/module/core/api/index.js.map +1 -0
  16. package/lib/module/core/types/index.js +4 -0
  17. package/lib/module/core/types/index.js.map +1 -0
  18. package/lib/module/engine/BLEReceiverService.js +190 -0
  19. package/lib/module/engine/BLEReceiverService.js.map +1 -0
  20. package/lib/module/engine/BLESenderService.js +259 -0
  21. package/lib/module/engine/BLESenderService.js.map +1 -0
  22. package/lib/module/engine/FPEngine.js +340 -0
  23. package/lib/module/engine/FPEngine.js.map +1 -0
  24. package/lib/module/engine/NearbyUsersService.js +87 -0
  25. package/lib/module/engine/NearbyUsersService.js.map +1 -0
  26. package/lib/module/index.js +16 -0
  27. package/lib/module/index.js.map +1 -0
  28. package/lib/module/package.json +1 -0
  29. package/lib/module/ui/components/FPButton.js +71 -0
  30. package/lib/module/ui/components/FPButton.js.map +1 -0
  31. package/lib/module/ui/components/LoadingAnimation/InLoading.js +74 -0
  32. package/lib/module/ui/components/LoadingAnimation/InLoading.js.map +1 -0
  33. package/lib/module/ui/components/LoadingAnimation/index.js +82 -0
  34. package/lib/module/ui/components/LoadingAnimation/index.js.map +1 -0
  35. package/lib/module/ui/components/OtpInput/OTPInputView.js +290 -0
  36. package/lib/module/ui/components/OtpInput/OTPInputView.js.map +1 -0
  37. package/lib/module/ui/components/OtpInput/Styles.js +20 -0
  38. package/lib/module/ui/components/OtpInput/Styles.js.map +1 -0
  39. package/lib/module/ui/components/OtpInput/helpers/codeToArray.js +7 -0
  40. package/lib/module/ui/components/OtpInput/helpers/codeToArray.js.map +1 -0
  41. package/lib/module/ui/components/OtpInput/helpers/device.js +9 -0
  42. package/lib/module/ui/components/OtpInput/helpers/device.js.map +1 -0
  43. package/lib/module/ui/components/OtpInput/helpers/styles.js +17 -0
  44. package/lib/module/ui/components/OtpInput/helpers/styles.js.map +1 -0
  45. package/lib/module/ui/components/OtpInput/helpers/types.js +4 -0
  46. package/lib/module/ui/components/OtpInput/helpers/types.js.map +1 -0
  47. package/lib/module/ui/components/OtpInput/index.js +45 -0
  48. package/lib/module/ui/components/OtpInput/index.js.map +1 -0
  49. package/lib/module/ui/components/PulseAnimation.js +61 -0
  50. package/lib/module/ui/components/PulseAnimation.js.map +1 -0
  51. package/lib/module/ui/modals/FPPaymentRequestModal.js +253 -0
  52. package/lib/module/ui/modals/FPPaymentRequestModal.js.map +1 -0
  53. package/lib/module/ui/modals/FPShell.js +180 -0
  54. package/lib/module/ui/modals/FPShell.js.map +1 -0
  55. package/lib/module/ui/screens/ReceiveScreen.js +291 -0
  56. package/lib/module/ui/screens/ReceiveScreen.js.map +1 -0
  57. package/lib/module/ui/screens/SendScreen.js +216 -0
  58. package/lib/module/ui/screens/SendScreen.js.map +1 -0
  59. package/lib/module/ui/screens/sub/BluetoothSubScreen.js +403 -0
  60. package/lib/module/ui/screens/sub/BluetoothSubScreen.js.map +1 -0
  61. package/lib/module/ui/screens/sub/NFCSubScreen.js +169 -0
  62. package/lib/module/ui/screens/sub/NFCSubScreen.js.map +1 -0
  63. package/lib/module/ui/screens/sub/NQRSubScreen.js +136 -0
  64. package/lib/module/ui/screens/sub/NQRSubScreen.js.map +1 -0
  65. package/lib/module/ui/screens/sub/ProximitySubScreen.js +501 -0
  66. package/lib/module/ui/screens/sub/ProximitySubScreen.js.map +1 -0
  67. package/lib/module/ui/screens/sub/TransferSubScreen.js +361 -0
  68. package/lib/module/ui/screens/sub/TransferSubScreen.js.map +1 -0
  69. package/lib/module/ui/theme/index.js +64 -0
  70. package/lib/module/ui/theme/index.js.map +1 -0
  71. package/lib/module/useFountainPay.js +82 -0
  72. package/lib/module/useFountainPay.js.map +1 -0
  73. package/lib/typescript/package.json +1 -0
  74. package/lib/typescript/src/FountainPayProvider.d.ts +7 -0
  75. package/lib/typescript/src/FountainPayProvider.d.ts.map +1 -0
  76. package/lib/typescript/src/core/api/client.d.ts +7 -0
  77. package/lib/typescript/src/core/api/client.d.ts.map +1 -0
  78. package/lib/typescript/src/core/api/index.d.ts +67 -0
  79. package/lib/typescript/src/core/api/index.d.ts.map +1 -0
  80. package/lib/typescript/src/core/types/index.d.ts +130 -0
  81. package/lib/typescript/src/core/types/index.d.ts.map +1 -0
  82. package/lib/typescript/src/engine/BLEReceiverService.d.ts +43 -0
  83. package/lib/typescript/src/engine/BLEReceiverService.d.ts.map +1 -0
  84. package/lib/typescript/src/engine/BLESenderService.d.ts +39 -0
  85. package/lib/typescript/src/engine/BLESenderService.d.ts.map +1 -0
  86. package/lib/typescript/src/engine/FPEngine.d.ts +24 -0
  87. package/lib/typescript/src/engine/FPEngine.d.ts.map +1 -0
  88. package/lib/typescript/src/engine/NearbyUsersService.d.ts +19 -0
  89. package/lib/typescript/src/engine/NearbyUsersService.d.ts.map +1 -0
  90. package/lib/typescript/src/index.d.ts +4 -0
  91. package/lib/typescript/src/index.d.ts.map +1 -0
  92. package/lib/typescript/src/ui/components/FPButton.d.ts +12 -0
  93. package/lib/typescript/src/ui/components/FPButton.d.ts.map +1 -0
  94. package/lib/typescript/src/ui/components/LoadingAnimation/InLoading.d.ts +7 -0
  95. package/lib/typescript/src/ui/components/LoadingAnimation/InLoading.d.ts.map +1 -0
  96. package/lib/typescript/src/ui/components/LoadingAnimation/index.d.ts +6 -0
  97. package/lib/typescript/src/ui/components/LoadingAnimation/index.d.ts.map +1 -0
  98. package/lib/typescript/src/ui/components/OtpInput/OTPInputView.d.ts +29 -0
  99. package/lib/typescript/src/ui/components/OtpInput/OTPInputView.d.ts.map +1 -0
  100. package/lib/typescript/src/ui/components/OtpInput/Styles.d.ts +330 -0
  101. package/lib/typescript/src/ui/components/OtpInput/Styles.d.ts.map +1 -0
  102. package/lib/typescript/src/ui/components/OtpInput/helpers/codeToArray.d.ts +6 -0
  103. package/lib/typescript/src/ui/components/OtpInput/helpers/codeToArray.d.ts.map +1 -0
  104. package/lib/typescript/src/ui/components/OtpInput/helpers/device.d.ts +6 -0
  105. package/lib/typescript/src/ui/components/OtpInput/helpers/device.d.ts.map +1 -0
  106. package/lib/typescript/src/ui/components/OtpInput/helpers/styles.d.ts +6 -0
  107. package/lib/typescript/src/ui/components/OtpInput/helpers/styles.d.ts.map +1 -0
  108. package/lib/typescript/src/ui/components/OtpInput/helpers/types.d.ts +84 -0
  109. package/lib/typescript/src/ui/components/OtpInput/helpers/types.d.ts.map +1 -0
  110. package/lib/typescript/src/ui/components/OtpInput/index.d.ts +9 -0
  111. package/lib/typescript/src/ui/components/OtpInput/index.d.ts.map +1 -0
  112. package/lib/typescript/src/ui/components/PulseAnimation.d.ts +2 -0
  113. package/lib/typescript/src/ui/components/PulseAnimation.d.ts.map +1 -0
  114. package/lib/typescript/src/ui/modals/FPPaymentRequestModal.d.ts +2 -0
  115. package/lib/typescript/src/ui/modals/FPPaymentRequestModal.d.ts.map +1 -0
  116. package/lib/typescript/src/ui/modals/FPShell.d.ts +2 -0
  117. package/lib/typescript/src/ui/modals/FPShell.d.ts.map +1 -0
  118. package/lib/typescript/src/ui/screens/ReceiveScreen.d.ts +10 -0
  119. package/lib/typescript/src/ui/screens/ReceiveScreen.d.ts.map +1 -0
  120. package/lib/typescript/src/ui/screens/SendScreen.d.ts +9 -0
  121. package/lib/typescript/src/ui/screens/SendScreen.d.ts.map +1 -0
  122. package/lib/typescript/src/ui/screens/sub/BluetoothSubScreen.d.ts +552 -0
  123. package/lib/typescript/src/ui/screens/sub/BluetoothSubScreen.d.ts.map +1 -0
  124. package/lib/typescript/src/ui/screens/sub/NFCSubScreen.d.ts +19 -0
  125. package/lib/typescript/src/ui/screens/sub/NFCSubScreen.d.ts.map +1 -0
  126. package/lib/typescript/src/ui/screens/sub/NQRSubScreen.d.ts +13 -0
  127. package/lib/typescript/src/ui/screens/sub/NQRSubScreen.d.ts.map +1 -0
  128. package/lib/typescript/src/ui/screens/sub/ProximitySubScreen.d.ts +552 -0
  129. package/lib/typescript/src/ui/screens/sub/ProximitySubScreen.d.ts.map +1 -0
  130. package/lib/typescript/src/ui/screens/sub/TransferSubScreen.d.ts +12 -0
  131. package/lib/typescript/src/ui/screens/sub/TransferSubScreen.d.ts.map +1 -0
  132. package/lib/typescript/src/ui/theme/index.d.ts +62 -0
  133. package/lib/typescript/src/ui/theme/index.d.ts.map +1 -0
  134. package/lib/typescript/src/useFountainPay.d.ts +3 -0
  135. package/lib/typescript/src/useFountainPay.d.ts.map +1 -0
  136. package/package.json +217 -0
  137. package/src/FountainPayProvider.tsx +21 -0
  138. package/src/core/api/client.ts +47 -0
  139. package/src/core/api/index.ts +61 -0
  140. package/src/core/types/index.ts +144 -0
  141. package/src/engine/BLEReceiverService.ts +244 -0
  142. package/src/engine/BLESenderService.ts +314 -0
  143. package/src/engine/FPEngine.ts +370 -0
  144. package/src/engine/NearbyUsersService.ts +106 -0
  145. package/src/index.ts +30 -0
  146. package/src/ui/components/FPButton.tsx +42 -0
  147. package/src/ui/components/LoadingAnimation/InLoading.tsx +88 -0
  148. package/src/ui/components/LoadingAnimation/index.tsx +93 -0
  149. package/src/ui/components/OtpInput/OTPInputView.tsx +243 -0
  150. package/src/ui/components/OtpInput/Styles.ts +19 -0
  151. package/src/ui/components/OtpInput/helpers/codeToArray.ts +3 -0
  152. package/src/ui/components/OtpInput/helpers/device.ts +6 -0
  153. package/src/ui/components/OtpInput/helpers/styles.ts +17 -0
  154. package/src/ui/components/OtpInput/helpers/types.ts +88 -0
  155. package/src/ui/components/OtpInput/index.tsx +51 -0
  156. package/src/ui/components/PulseAnimation.tsx +78 -0
  157. package/src/ui/modals/FPPaymentRequestModal.tsx +158 -0
  158. package/src/ui/modals/FPShell.tsx +107 -0
  159. package/src/ui/screens/ReceiveScreen.tsx +119 -0
  160. package/src/ui/screens/SendScreen.tsx +86 -0
  161. package/src/ui/screens/sub/BluetoothSubScreen.tsx +433 -0
  162. package/src/ui/screens/sub/NFCSubScreen.tsx +83 -0
  163. package/src/ui/screens/sub/NQRSubScreen.tsx +61 -0
  164. package/src/ui/screens/sub/ProximitySubScreen.tsx +390 -0
  165. package/src/ui/screens/sub/TransferSubScreen.tsx +146 -0
  166. package/src/ui/theme/index.ts +24 -0
  167. package/src/useFountainPay.ts +95 -0
@@ -0,0 +1,390 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import styled from 'styled-components/native';
3
+ import Svg, { Path } from "react-native-svg";
4
+ import { View, Text, FlatList, TouchableOpacity, StyleSheet, ActivityIndicator, TextInput, Animated, Easing } from 'react-native';
5
+ import { proximityAPI, transferAPI } from '../../../core/api';
6
+ import { FPButton } from '../../components/FPButton';
7
+ import { C, R, S, F, shadow } from '../../theme';
8
+ import type { FPCurrency, FPProximityPeer, FPError, FPTransaction } from '../../../core/types';
9
+ import { FPEngine } from '../../../engine/FPEngine';
10
+ import NearbyUsersService from '../../../engine/NearbyUsersService';
11
+ import { PulseAnimation } from '../../components/PulseAnimation';
12
+ import Ionicons from 'react-native-vector-icons/Ionicons';
13
+ import LoadingAnimation from '../../components/LoadingAnimation';
14
+
15
+
16
+ const Container = styled(View)`
17
+ flex: 1;
18
+ background-color: #000000;
19
+ `;
20
+
21
+ const Header = styled(View)`
22
+ flex-direction: row;
23
+ align-items: center;
24
+ justify-content: space-between;
25
+ padding: 12px 16px;
26
+ position: absolute !important;
27
+ top: 40px;
28
+ left: 0;
29
+ right: 0;
30
+ z-index: 10;
31
+ elevation: 4;
32
+ shadow-color: #000;
33
+ shadow-offset: 0px 2px;
34
+ shadow-opacity: 0.1;
35
+ shadow-radius: 4px;
36
+ z-index: 9999999999 !important;
37
+ `;
38
+
39
+ const HeaderButton = styled(TouchableOpacity)`
40
+ padding: 8px;
41
+ background-color: #fff;
42
+ width: 40px;
43
+ height: 40px;
44
+ border-radius: 50%;
45
+ justify-content: center;
46
+ align-items: center;
47
+ `;
48
+
49
+ const HeaderCenter = styled(View)`
50
+ flex-direction: row;
51
+ align-items: center;
52
+ gap: 8px;
53
+ `;
54
+
55
+ const HeaderRight = styled(View)`
56
+ flex-direction: row;
57
+ align-items: center;
58
+ gap: 8px;
59
+ `;
60
+
61
+ const QuickLinksContainer = styled.View`
62
+ align-items: center;
63
+ margin-top: 250px;
64
+ `;
65
+
66
+ const QuickLinksContainerWithDevices = styled.View`
67
+ align-items: start;
68
+ margin-top: 30%;
69
+ margin-bottom: 20px;
70
+ `;
71
+
72
+ const QuickLinksIcon = styled.TouchableOpacity`
73
+ width: 96px;
74
+ height: 96px;
75
+ border-radius: 48px;
76
+ background-color: #18181b;
77
+ justify-content: center;
78
+ align-items: center;
79
+ margin-bottom: 16px;
80
+ `;
81
+
82
+ const QuickLinksText = styled.Text`
83
+ color: #9ca3af;
84
+ font-size: 18px;
85
+ `;
86
+
87
+ export const DeviceCard = styled.View`
88
+ background-color: white;
89
+ border-radius: 12px;
90
+ padding: 16px;
91
+ margin-bottom: 12px;
92
+ flex-direction: row;
93
+ justify-content: space-between;
94
+ align-items: center;
95
+ shadow-color: #000;
96
+ shadow-offset: 0px 2px;
97
+ shadow-opacity: 0.1;
98
+ shadow-radius: 8px;
99
+ elevation: 3;
100
+ `;
101
+
102
+ export const DeviceInfo = styled.View`
103
+ flex: 1;
104
+ `;
105
+
106
+ export const DeviceName = styled.Text`
107
+ font-size: 16px;
108
+ font-weight: 600;
109
+ color: #111827;
110
+ margin-bottom: 4px;
111
+ `;
112
+
113
+ export const DeviceSignal = styled.Text`
114
+ font-size: 14px;
115
+ color: #6b7280;
116
+ `;
117
+
118
+ export const PayButton = styled.TouchableOpacity`
119
+ background-color: #2563eb;
120
+ padding: 10px 20px;
121
+ border-radius: 8px;
122
+ `;
123
+
124
+ export const PayButtonText = styled.Text`
125
+ color: white;
126
+ font-size: 14px;
127
+ font-weight: 600;
128
+ `;
129
+
130
+ const ScrollContainer = styled.ScrollView`
131
+ flex: 1;
132
+ `;
133
+
134
+ const ContentContainer = styled.View`
135
+ padding: 14px;
136
+ margin-bottom: 80%;
137
+ `;
138
+
139
+ const BluetoothIconContainer = styled.View`
140
+ align-items: center;
141
+ margin-top: 15%;
142
+ `;
143
+
144
+ const LCAnimationContainer = styled.View`
145
+ align-items: center;
146
+ justify-content: center;
147
+ margin-top: 40%;
148
+ `;
149
+
150
+ const LCIconContainer = styled(Animated.View)`
151
+ width: 140px;
152
+ height: 140px;
153
+ border-radius: 90px;
154
+ background-color: #E8F5F1;
155
+ align-items: center;
156
+ justify-content: center;
157
+ margin-top: 24px;
158
+ margin-bottom: 24px;
159
+ `;
160
+
161
+ const LCIcon = styled.Text`
162
+ font-size: 80px;
163
+ `;
164
+
165
+ const StatusTextContainer = styled.View`
166
+ align-items: center;
167
+ margin-bottom: 8px;
168
+ `;
169
+
170
+ const StatusText = styled.Text`
171
+ font-size: 18px;
172
+ font-weight: 600;
173
+ color: #333333;
174
+ text-align: center;
175
+ margin-bottom: 8px;
176
+ `;
177
+
178
+ const StatusSubText = styled.Text`
179
+ font-size: 14px;
180
+ color: #666666;
181
+ text-align: center;
182
+ `;
183
+
184
+ const ConnectionStatusBadge = styled.View<{ connected: boolean }>`
185
+ background-color: ${props => props.connected ? '#0a3d2e' : '#E0E0E0'};
186
+ border-radius: 20px;
187
+ padding: 8px 16px;
188
+ margin-top: 16px;
189
+ `;
190
+
191
+ const ConnectionStatusText = styled.Text<{ connected: boolean }>`
192
+ font-size: 12px;
193
+ font-weight: 600;
194
+ color: ${props => props.connected ? '#ffffff' : '#666666'};
195
+ `;
196
+
197
+
198
+ const BluetoothIcon =({ color = "#111", size = 22 })=>(
199
+ <Svg width={size} height={size} strokeWidth="0.9" viewBox="0 0 24 24" fill="none" color={color}>
200
+ <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" />
201
+ <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" />
202
+ <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" />
203
+ <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" />
204
+ <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" />
205
+ </Svg>
206
+ );
207
+
208
+
209
+ const BluetoothScanningIcon = ({ color = "#111", size = 22})=>(
210
+ <Svg width={size} height={size} strokeWidth="0.9" viewBox="0 0 24 24" fill="none" color={color}>
211
+ <Path d="M6.75 8L17.25 16.5L11.75 22V2L17.25 7.5L6.75 16" stroke={color} strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
212
+ </Svg>
213
+ )
214
+
215
+ const ProximityIcon =({ color = "#111", size = 22 })=>(
216
+ <Svg width={size} height={size} strokeWidth="0.9" viewBox="0 0 24 24" fill="none" color={color}>
217
+ <Path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke={color} strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round"/>
218
+ <Path d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" fill={color} stroke={color} strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round"/>
219
+ <Path d="M19 19L17.5 17.5" stroke={color} strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round"/>
220
+ <Path d="M15.5 15.5L14.5 14.5" stroke={color} strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round"/>
221
+ </Svg>
222
+ )
223
+
224
+ interface Props {
225
+ amount: number; currency: FPCurrency;
226
+ onBack: () => void; onDone: () => void;
227
+ onSuccess?: (tx: FPTransaction) => void; onError?: (err: FPError) => void;
228
+ }
229
+
230
+ export function ProximitySubScreen({ amount, currency, onDone, onSuccess, onError }: Props) {
231
+
232
+ const [nearbyUsers, setNearbyUsers] = useState([]);
233
+ const [loading, setLoading] = useState(false);
234
+ const [scanning, setScanning] = useState(false);
235
+ const iconScale = useRef(new Animated.Value(1)).current;
236
+ const animationRef = useRef<Animated.CompositeAnimation | null>(null);
237
+
238
+ const handleScan = async () => {
239
+ try {
240
+ const users: any = await NearbyUsersService.getNearbyUsers();
241
+ setNearbyUsers(users);
242
+ } catch (error) {
243
+ console.error('Error fetching nearby users:', error);
244
+ } finally {
245
+ setScanning(false);
246
+ }
247
+ };
248
+
249
+ const handlePayment = async (selected: any) => {
250
+ if (!selected) return;
251
+
252
+ setLoading(true);
253
+ try {
254
+ const result = await transferAPI.send({ accountNumber: selected.accountNumber, bankCode: selected.bankCode, amount: amount * 100, narration: 'Proximity payment' });
255
+ onSuccess?.({ id: result.reference, reference: result.reference, type: 'debit', channel: 'proximity', amount: result.amount, currency: result.currency, status: result.status, recipient: { accountName: selected.displayName }, createdAt: result.createdAt });
256
+ onDone();
257
+ } catch (e) { onError?.(e as FPError); }
258
+ finally { setLoading(false); }
259
+ };
260
+
261
+ useEffect(() => {
262
+ if (scanning) {
263
+ handleScan();
264
+ animationRef.current = Animated.loop(
265
+ Animated.sequence([
266
+ Animated.timing(iconScale, {
267
+ toValue: 1.1,
268
+ duration: 1500,
269
+ easing: Easing.inOut(Easing.ease),
270
+ useNativeDriver: true,
271
+ }),
272
+ Animated.timing(iconScale, {
273
+ toValue: 1,
274
+ duration: 1500,
275
+ easing: Easing.inOut(Easing.ease),
276
+ useNativeDriver: true,
277
+ }),
278
+ ])
279
+ );
280
+
281
+ animationRef.current.start();
282
+ } else {
283
+ // Stop animation cleanly
284
+ animationRef.current?.stop();
285
+ iconScale.stopAnimation();
286
+ iconScale.setValue(1);
287
+ }
288
+
289
+ return () => {
290
+ animationRef.current?.stop();
291
+ };
292
+ }, [scanning]);
293
+
294
+ return (
295
+ <Container>
296
+ {/* Status Bar */}
297
+ {loading && (
298
+ <LoadingAnimation />
299
+ )}
300
+ <Header>
301
+ <HeaderCenter>
302
+ <Text style={{ fontWeight: 'bold', fontSize: 16, color:'#FFF' }}>Proximity</Text>
303
+ <Text style={{ fontSize: 10, color: '#FFF' }}>Transfer</Text>
304
+ </HeaderCenter>
305
+
306
+ <HeaderRight>
307
+ <HeaderButton>
308
+ <Ionicons name="ellipsis-vertical" size={22} />
309
+ </HeaderButton>
310
+ </HeaderRight>
311
+ </Header>
312
+
313
+ {!scanning && !nearbyUsers.length && (
314
+ <QuickLinksContainer>
315
+ <QuickLinksIcon onPress={()=>setScanning(true)}>
316
+ <ProximityIcon size={60} color="#FFF" />
317
+ </QuickLinksIcon>
318
+ <QuickLinksText>Tap to Pay</QuickLinksText>
319
+ <Text style={{ fontSize: 16, color: '#999' }}>
320
+ No nearby users found
321
+ </Text>
322
+ </QuickLinksContainer>
323
+ )}
324
+
325
+ {scanning && (
326
+ <LCAnimationContainer>
327
+ <PulseAnimation />
328
+ <LCIconContainer style={{ transform: [{ scale: iconScale }], marginTop: 64 }}>
329
+ <LCIcon>📡</LCIcon>
330
+ </LCIconContainer>
331
+
332
+ <StatusTextContainer>
333
+ <StatusText>
334
+ Searching for nearby users
335
+ </StatusText>
336
+ {/* <StatusSubText>Hold devices back-to-back</StatusSubText> */}
337
+ </StatusTextContainer>
338
+ </LCAnimationContainer>
339
+
340
+ )}
341
+
342
+ {!scanning && nearbyUsers.length > 0 && (
343
+ <ScrollContainer showsVerticalScrollIndicator={false}>
344
+ <ContentContainer>
345
+ <QuickLinksContainerWithDevices>
346
+ <QuickLinksText>Found {nearbyUsers.length} user(s)</QuickLinksText>
347
+ </QuickLinksContainerWithDevices>
348
+ {nearbyUsers.map((user: any) => (
349
+ <DeviceCard key={user.userId}>
350
+ <DeviceInfo>
351
+ <DeviceName>
352
+ {user.userName || user.name}
353
+ {user.appName}
354
+ </DeviceName>
355
+ <DeviceSignal>
356
+ Signal: {Math.round(user.distance)}m away
357
+ </DeviceSignal>
358
+ </DeviceInfo>
359
+ <PayButton
360
+ onPress={() => handlePayment(user)}
361
+ activeOpacity={0.8}
362
+ >
363
+ <PayButtonText>Pay</PayButtonText>
364
+ </PayButton>
365
+ </DeviceCard>
366
+ ))}
367
+ </ContentContainer>
368
+ </ScrollContainer>
369
+
370
+ )}
371
+ </Container>
372
+ );
373
+ }
374
+
375
+ const st = StyleSheet.create({
376
+ wrap: { flex: 1, padding: S.lg },
377
+ back: { color: C.brand, fontSize: F.md, fontWeight: '600', marginBottom: S.md },
378
+ title: { fontSize: F.xl, fontWeight: '800', color: C.ink, marginBottom: S.lg },
379
+ peerCard: { flexDirection: 'row', alignItems: 'center', backgroundColor: C.surface, borderRadius: R.lg, padding: S.md, borderWidth: 2, borderColor: 'transparent' },
380
+ peerSelected: { borderColor: C.brand, backgroundColor: '#EEF4FF' },
381
+ avatar: { width: 44, height: 44, borderRadius: 22, backgroundColor: C.brand, justifyContent: 'center', alignItems: 'center', marginRight: S.md },
382
+ avatarText: { color: C.white, fontWeight: '800', fontSize: F.lg },
383
+ peerName: { fontSize: F.md, fontWeight: '700', color: C.ink },
384
+ peerBank: { fontSize: F.sm, color: C.muted },
385
+ dist: { fontSize: F.xs, color: C.brand },
386
+ check: { color: C.brand, fontWeight: '800' },
387
+ empty: { textAlign: 'center', color: C.muted, padding: S.xl },
388
+ footer: { backgroundColor: C.surface, borderRadius: R.lg, padding: S.lg, marginTop: S.md },
389
+ footerText: { fontSize: F.md, color: C.ink, fontWeight: '600', marginBottom: S.md },
390
+ });
@@ -0,0 +1,146 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { View, Text, TextInput, FlatList, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native';
3
+ import { transferAPI } from '../../../core/api';
4
+ import { FPButton } from '../../components/FPButton';
5
+ import { C, R, S, F, shadow } from '../../theme';
6
+ import type { FPCurrency, FPTransaction, FPError } from '../../../core/types';
7
+
8
+ type Step = 'bank' | 'account' | 'confirm' | 'processing' | 'done';
9
+
10
+ interface Props {
11
+ amount: number; currency: FPCurrency;
12
+ onBack: () => void; onDone: () => void;
13
+ onSuccess?: (tx: FPTransaction) => void; onError?: (err: FPError) => void;
14
+ }
15
+
16
+ export function TransferSubScreen({ amount, currency, onBack, onDone, onSuccess, onError }: Props) {
17
+ const [step, setStep] = useState<Step>('bank');
18
+ const [banks, setBanks] = useState<{ name: string; code: string }[]>([]);
19
+ const [search, setSearch] = useState('');
20
+ const [selectedBank, setSelectedBank] = useState<{ name: string; code: string } | null>(null);
21
+ const [accountNumber, setAccountNumber] = useState('');
22
+ const [verifiedName, setVerifiedName] = useState('');
23
+ const [isVerifying, setIsVerifying] = useState(false);
24
+ const [narration, setNarration] = useState('');
25
+ const [loading, setLoading] = useState(false);
26
+ const [error, setError] = useState('');
27
+
28
+ useEffect(() => { transferAPI.getBanks().then(setBanks).catch(() => {}); }, []);
29
+
30
+ useEffect(() => {
31
+ if (accountNumber.length === 10 && selectedBank) {
32
+ setIsVerifying(true); setVerifiedName('');
33
+ transferAPI.verifyAccount(accountNumber, selectedBank.code)
34
+ .then(r => setVerifiedName(r.accountName))
35
+ .catch(() => setVerifiedName(''))
36
+ .finally(() => setIsVerifying(false));
37
+ }
38
+ }, [accountNumber, selectedBank]);
39
+
40
+ const handleSend = async () => {
41
+ if (!selectedBank || !verifiedName) return;
42
+ setLoading(true); setStep('processing');
43
+ try {
44
+ const result = await transferAPI.send({ accountNumber, bankCode: selectedBank.code, amount: amount * 100, narration });
45
+ onSuccess?.({ id: result.reference, reference: result.reference, type: 'debit', channel: 'transfer', amount: result.amount, currency: result.currency, status: result.status, recipient: { accountName: result.recipient.accountName }, createdAt: result.createdAt });
46
+ setStep('done');
47
+ } catch (err) {
48
+ setError((err as FPError).message); setStep('confirm');
49
+ onError?.(err as FPError);
50
+ } finally { setLoading(false); }
51
+ };
52
+
53
+ const filtered = banks.filter(b => b.name.toLowerCase().includes(search.toLowerCase()));
54
+
55
+ if (step === 'bank') return (
56
+ <View style={st.wrap}>
57
+ <TouchableOpacity onPress={onBack} style={st.back}><Text style={st.backText}>Back</Text></TouchableOpacity>
58
+ <Text style={st.title}>Select Bank</Text>
59
+ <TextInput style={st.input} placeholder="Search..." value={search} onChangeText={setSearch} placeholderTextColor={C.ghost} />
60
+ <FlatList data={filtered} keyExtractor={i => i.code}
61
+ renderItem={({ item }) => (
62
+ <TouchableOpacity style={st.bankRow} onPress={() => { setSelectedBank(item); setStep('account'); }}>
63
+ <Text style={st.bankName}>{item.name}</Text><Text style={st.arrow}>{">"}</Text>
64
+ </TouchableOpacity>
65
+ )}
66
+ ItemSeparatorComponent={() => <View style={{ height: 1, backgroundColor: C.border }} />}
67
+ />
68
+ </View>
69
+ );
70
+
71
+ if (step === 'account') return (
72
+ <View style={st.wrap}>
73
+ <TouchableOpacity onPress={() => setStep('bank')} style={st.back}><Text style={st.backText}>{selectedBank?.name}</Text></TouchableOpacity>
74
+ <Text style={st.title}>Account Number</Text>
75
+ <TextInput style={st.bigInput} placeholder="0000000000" value={accountNumber}
76
+ onChangeText={v => setAccountNumber(v.replace(/[^0-9]/g, '').slice(0, 10))}
77
+ keyboardType="number-pad" maxLength={10} placeholderTextColor={C.ghost} />
78
+ {isVerifying && <ActivityIndicator color={C.brand} />}
79
+ {verifiedName ? <View style={st.verified}><Text style={st.verifiedText}>Verified: {verifiedName}</Text></View> : null}
80
+ <FPButton label="Continue" onPress={() => setStep('confirm')} disabled={!verifiedName} style={st.btn} />
81
+ </View>
82
+ );
83
+
84
+ if (step === 'confirm') return (
85
+ <View style={st.wrap}>
86
+ <TouchableOpacity onPress={() => setStep('account')} style={st.back}><Text style={st.backText}>Back</Text></TouchableOpacity>
87
+ <Text style={st.title}>Confirm Transfer</Text>
88
+ <View style={st.card}>
89
+ <Row label="Recipient" value={verifiedName} />
90
+ <Row label="Bank" value={selectedBank?.name ?? ''} />
91
+ <Row label="Account" value={accountNumber} />
92
+ <Row label="Amount" value={currency + ' ' + amount.toLocaleString('en-NG', { minimumFractionDigits: 2 })} bold />
93
+ </View>
94
+ <TextInput style={st.input} placeholder="Add a note (optional)" value={narration} onChangeText={setNarration} placeholderTextColor={C.ghost} />
95
+ {error ? <Text style={st.error}>{error}</Text> : null}
96
+ <FPButton label="Send Money" onPress={handleSend} loading={loading} style={st.btn} />
97
+ </View>
98
+ );
99
+
100
+ if (step === 'processing') return (
101
+ <View style={[st.wrap, st.centered]}>
102
+ <ActivityIndicator size="large" color={C.brand} />
103
+ <Text style={st.title}>Sending...</Text>
104
+ </View>
105
+ );
106
+
107
+ if (step === 'done') return (
108
+ <View style={[st.wrap, st.centered]}>
109
+ <View style={st.successCircle}><Text style={{ fontSize: 28, color: C.green }}>OK</Text></View>
110
+ <Text style={st.title}>Sent!</Text>
111
+ <Text style={st.sub}>{currency} {amount.toLocaleString()} to {verifiedName}</Text>
112
+ <FPButton label="Done" onPress={onDone} style={st.btn} />
113
+ </View>
114
+ );
115
+
116
+ return null;
117
+ }
118
+
119
+ function Row({ label, value, bold }: { label: string; value: string; bold?: boolean }) {
120
+ return (
121
+ <View style={{ flexDirection: 'row', justifyContent: 'space-between', paddingVertical: S.sm, borderBottomWidth: 1, borderBottomColor: C.border }}>
122
+ <Text style={{ color: C.muted, fontSize: F.sm }}>{label}</Text>
123
+ <Text style={{ color: C.ink, fontWeight: bold ? '800' : '600', fontSize: F.sm }}>{value}</Text>
124
+ </View>
125
+ );
126
+ }
127
+
128
+ const st = StyleSheet.create({
129
+ wrap: { flex: 1, padding: S.lg },
130
+ centered: { justifyContent: 'center', alignItems: 'center' },
131
+ back: { marginBottom: S.md },
132
+ backText: { color: C.brand, fontSize: F.md, fontWeight: '600' },
133
+ title: { fontSize: F.xl, fontWeight: '800', color: C.ink, marginBottom: S.lg },
134
+ input: { backgroundColor: C.surface, borderRadius: R.md, padding: S.md, fontSize: F.md, color: C.ink, borderWidth: 1, borderColor: C.border, marginBottom: S.md },
135
+ bigInput: { backgroundColor: C.surface, borderRadius: R.md, padding: S.md, fontSize: 28, fontWeight: '800', color: C.ink, textAlign: 'center', letterSpacing: 3, borderWidth: 1, borderColor: C.border, marginBottom: S.md },
136
+ verified: { backgroundColor: C.greenLight, borderRadius: R.md, padding: S.sm, marginBottom: S.md },
137
+ verifiedText: { color: C.green, fontWeight: '700', fontSize: F.sm },
138
+ btn: { marginTop: S.sm },
139
+ bankRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: S.md, alignItems: 'center' },
140
+ bankName: { fontSize: F.md, color: C.ink },
141
+ arrow: { color: C.ghost, fontSize: F.lg },
142
+ card: { backgroundColor: C.surface, borderRadius: R.lg, padding: S.md, marginBottom: S.md },
143
+ error: { color: C.red, fontSize: F.sm, marginBottom: S.sm },
144
+ successCircle: { width: 80, height: 80, borderRadius: 40, backgroundColor: C.greenLight, justifyContent: 'center', alignItems: 'center', marginBottom: S.lg },
145
+ sub: { color: C.muted, fontSize: F.md, marginBottom: S.xl },
146
+ });
@@ -0,0 +1,24 @@
1
+ export const C = {
2
+ ink: '#0B1D35',
3
+ brand: '#0052CC',
4
+ brandLight: '#E8F0FE',
5
+ green: '#00875A',
6
+ greenLight: '#E3FCEF',
7
+ red: '#DE350B',
8
+ redLight: '#FFEBE6',
9
+ amber: '#FF8B00',
10
+ white: '#FFFFFF',
11
+ surface: '#F4F5F7',
12
+ border: '#DFE1E6',
13
+ muted: '#6B778C',
14
+ ghost: '#B3BAC5',
15
+ };
16
+
17
+ export const R = { sm: 8, md: 12, lg: 16, xl: 20, full: 999 };
18
+ export const S = { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48 };
19
+ export const F = { xs: 11, sm: 13, md: 15, lg: 17, xl: 20, xxl: 24, hero: 32 };
20
+
21
+ export const shadow = {
22
+ sm: { shadowColor: '#0B1D35', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.07, shadowRadius: 6, elevation: 2 },
23
+ lg: { shadowColor: '#0B1D35', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.12, shadowRadius: 20, elevation: 8 },
24
+ };
@@ -0,0 +1,95 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // useFountainPay — The ONLY hook the host app ever uses.
3
+ //
4
+ // Usage:
5
+ // const pay = useFountainPay('your-api-key');
6
+ // await pay.initializeSDK(userInfo, callbacks);
7
+ // pay.send(500, 'NGN');
8
+ // pay.receive(200, 'NGN');
9
+ // const acct = await pay.generateAccountNumber({ accountName: 'John' });
10
+ // pay.listen();
11
+ // pay.destroy();
12
+ // ─────────────────────────────────────────────────────────────
13
+ import { useMemo } from 'react';
14
+ import { FPEngine } from './engine/FPEngine';
15
+ import { initClient } from './core/api/client';
16
+ import type {
17
+ FPInstance,
18
+ FPUserInfo,
19
+ FPSDKOptions,
20
+ FPCallbacks,
21
+ FPCurrency,
22
+ FPGenerateAccountRequest,
23
+ FPVirtualAccount,
24
+ } from './core/types';
25
+
26
+ export function useFountainPay(apiKey: string, options?: FPSDKOptions): FPInstance {
27
+ // Initialize HTTP client immediately so API calls can be made
28
+ // even before initializeSDK (e.g. generateAccountNumber for onboarding)
29
+ useMemo(() => {
30
+ initClient(apiKey, { baseUrl: options?.baseUrl, environment: options?.environment });
31
+ }, [apiKey]);
32
+
33
+ const instance = useMemo<FPInstance>(() => ({
34
+ /**
35
+ * Call this once after the user logs in.
36
+ * Starts proximity broadcasting, BLE peripheral, and BT listener.
37
+ */
38
+ async initializeSDK(user: FPUserInfo, callbacks?: FPCallbacks): Promise<void> {
39
+ await FPEngine.initialize(apiKey, user, options ?? {}, callbacks ?? {});
40
+ },
41
+
42
+ /**
43
+ * Open the Send Money sheet.
44
+ * @param amount Major denomination (e.g. 500 for NGN 500)
45
+ * @param currency e.g. 'NGN', 'USD'
46
+ */
47
+ send(amount: number, currency: FPCurrency): void {
48
+ FPEngine.showSend(amount, currency);
49
+ },
50
+
51
+ /**
52
+ * Open the Receive Money sheet.
53
+ * @param amount Expected amount (optional)
54
+ * @param currency e.g. 'NGN'
55
+ */
56
+ receive(amount?: number, currency: FPCurrency = 'NGN'): void {
57
+ FPEngine.showReceive(amount, currency);
58
+ },
59
+
60
+ /**
61
+ * Generate a virtual account number.
62
+ * No UI is shown — returns the account directly.
63
+ */
64
+ async generateAccountNumber(request: FPGenerateAccountRequest): Promise<FPVirtualAccount> {
65
+ return FPEngine.generateAccount(request);
66
+ },
67
+
68
+ /**
69
+ * Start listening for incoming Bluetooth payment requests.
70
+ * When one arrives, the SDK auto-shows the Accept/Decline modal.
71
+ * The result fires onPaymentReceived / onPaymentDeclined callbacks
72
+ * that were passed into initializeSDK.
73
+ */
74
+ listen(): void {
75
+ FPEngine.startListening();
76
+ },
77
+
78
+ /**
79
+ * Stop all background services. Call on logout.
80
+ */
81
+ destroy(): void {
82
+ FPEngine.destroy();
83
+ },
84
+
85
+ get isReady(): boolean {
86
+ return FPEngine.isReady();
87
+ },
88
+
89
+ get currentUser(): FPUserInfo | null {
90
+ return FPEngine.getUser();
91
+ },
92
+ }), [apiKey]);
93
+
94
+ return instance;
95
+ }