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,244 @@
1
+ // FPay SDK — BLEReceiverService (Peripheral)
2
+ // Ported directly from your BLEReceiverService.ts.
3
+ // Same UUIDs, same event ordering, same write handler, same response flow.
4
+ // Only changes: removed useBoundStore dependency, appName is 'FountainPay'.
5
+
6
+ import { PermissionsAndroid, Platform } from 'react-native';
7
+ import Peripheral, { Permission, Property } from 'react-native-multi-ble-peripheral';
8
+ import { Buffer } from 'buffer';
9
+
10
+ // ── UUIDs — shared with BLESenderService ─────────────────────
11
+ export const FP_SERVICE_UUID = 'E8E66D12-C217-425A-AB36-DDBA22F22A5D';
12
+ export const FP_REQUEST_CHAR_UUID = 'E1CC0349-343A-4E12-A4EF-CF69A681EE86';
13
+ export const FP_RESPONSE_CHAR_UUID = 'DA9318CA-C740-4951-9C7C-5747FE002EF9';
14
+ export const FP_USERINFO_CHAR_UUID = '933CA5AE-4D53-4CF4-864A-2FF51AFA05E9';
15
+
16
+ // ── Types ─────────────────────────────────────────────────────
17
+
18
+ export interface BLEPaymentRequest {
19
+ amount: number;
20
+ currency: string;
21
+ senderId: string;
22
+ senderName: string;
23
+ timestamp: number;
24
+ }
25
+
26
+ export interface BLEPaymentResponse {
27
+ accepted: boolean;
28
+ transactionId?: string;
29
+ timestamp: number;
30
+ // Receiver's account details — sent back to the sender so they can
31
+ // process the bank transfer on their end
32
+ accountDetails?: {
33
+ accountName: string;
34
+ accountNumber: string;
35
+ bankName: string;
36
+ bankCode: string;
37
+ };
38
+ // Echo of the original request timestamp — used by sender's polling
39
+ // to confirm this response belongs to their specific request
40
+ requestTimestamp?: number;
41
+ }
42
+
43
+ export type PaymentRequestCallback = (request: BLEPaymentRequest, deviceId: string) => void;
44
+
45
+ // ── Class ─────────────────────────────────────────────────────
46
+
47
+ class BLEReceiverService {
48
+ private peripheral: any = null;
49
+ private isAdvertising = false;
50
+ private onRequestCallback: PaymentRequestCallback | null = null;
51
+ private userInfo = { appName: 'FountainPay', userName: '', userId: '' };
52
+
53
+ // ── Init ───────────────────────────────────────────────────
54
+
55
+ async initializeWithUserInfo(userId: string, userName: string, appName = 'FountainPay'): Promise<void> {
56
+ this.userInfo = { userId, userName, appName };
57
+ await this._requestPermissions();
58
+ }
59
+
60
+ private async _requestPermissions(): Promise<boolean> {
61
+ if (Platform.OS !== 'android') return true;
62
+ if (Platform.Version >= 31) {
63
+ const results = await PermissionsAndroid.requestMultiple([
64
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADVERTISE,
65
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
66
+ PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
67
+ ]);
68
+ return Object.values(results).every(r => r === PermissionsAndroid.RESULTS.GRANTED);
69
+ }
70
+ const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION);
71
+ return granted === PermissionsAndroid.RESULTS.GRANTED;
72
+ }
73
+
74
+ // ── Advertising ────────────────────────────────────────────
75
+
76
+ async startAdvertising(): Promise<void> {
77
+ if (this.isAdvertising) {
78
+ console.log('[FPay BLE Receiver] Already advertising');
79
+ return;
80
+ }
81
+
82
+ this.peripheral = new Peripheral();
83
+
84
+ // CRITICAL: Register event listeners BEFORE the 'ready' event fires.
85
+ // This mirrors your original code exactly — if you set up listeners
86
+ // inside the 'ready' callback, write events that fire immediately after
87
+ // setup can be missed.
88
+ this._setupEventListeners();
89
+
90
+ await new Promise<void>((resolve, reject) => {
91
+ const timeout = setTimeout(
92
+ () => reject(new Error('[FPay BLE Receiver] Setup timeout (10s)')),
93
+ 10000
94
+ );
95
+
96
+ this.peripheral.on('ready', async () => {
97
+ try {
98
+ clearTimeout(timeout);
99
+ console.log('[FPay BLE Receiver] Peripheral ready');
100
+
101
+ await Peripheral.setDeviceName(this.userInfo.userName);
102
+ console.log('[FPay BLE Receiver] Device name set:', this.userInfo.userName);
103
+
104
+ await this.peripheral.addService(FP_SERVICE_UUID, true);
105
+
106
+ // User info characteristic — READ only.
107
+ // Sender reads this to get display name and userId.
108
+ const userInfoPayload = JSON.stringify({
109
+ appName: this.userInfo.appName,
110
+ userName: this.userInfo.userName,
111
+ userId: this.userInfo.userId,
112
+ });
113
+ await this.peripheral.addCharacteristic(
114
+ FP_SERVICE_UUID, FP_USERINFO_CHAR_UUID,
115
+ Property.READ, Permission.READABLE
116
+ );
117
+ await this.peripheral.updateValue(
118
+ FP_SERVICE_UUID, FP_USERINFO_CHAR_UUID,
119
+ Buffer.from(userInfoPayload)
120
+ );
121
+
122
+ // Payment request characteristic — WRITE / WRITE_NO_RESPONSE.
123
+ // Sender writes the payment request JSON here.
124
+ await this.peripheral.addCharacteristic(
125
+ FP_SERVICE_UUID, FP_REQUEST_CHAR_UUID,
126
+ Property.WRITE | Property.WRITE_NO_RESPONSE,
127
+ Permission.WRITEABLE
128
+ );
129
+
130
+ // Payment response characteristic — READ + NOTIFY.
131
+ // Receiver writes accept/decline here; sender polls or subscribes.
132
+ await this.peripheral.addCharacteristic(
133
+ FP_SERVICE_UUID, FP_RESPONSE_CHAR_UUID,
134
+ Property.READ | Property.NOTIFY,
135
+ Permission.READABLE
136
+ );
137
+ // Seed a default value so the characteristic is always readable
138
+ // (prevents "characteristic not found" errors on first poll)
139
+ await this.peripheral.updateValue(
140
+ FP_SERVICE_UUID, FP_RESPONSE_CHAR_UUID,
141
+ Buffer.from(JSON.stringify({ accepted: false, timestamp: 0 }))
142
+ );
143
+
144
+ await this.peripheral.startAdvertising({
145
+ serviceUuids: [FP_SERVICE_UUID],
146
+ includeDeviceName: true,
147
+ });
148
+
149
+ this.isAdvertising = true;
150
+ console.log('[FPay BLE Receiver] Advertising as "' + this.userInfo.userName + '"');
151
+ resolve();
152
+ } catch (err) {
153
+ clearTimeout(timeout);
154
+ console.error('[FPay BLE Receiver] Setup error:', err);
155
+ reject(err);
156
+ }
157
+ });
158
+ });
159
+ }
160
+
161
+ // ── Event handling ─────────────────────────────────────────
162
+
163
+ private _setupEventListeners(): void {
164
+ if (!this.peripheral) return;
165
+
166
+ this.peripheral.on('write', (data: any) => {
167
+ console.log('[FPay BLE Receiver] Write event received');
168
+ if (data?.value) {
169
+ this._handlePaymentRequest(data);
170
+ } else {
171
+ console.warn('[FPay BLE Receiver] Write event had no value');
172
+ }
173
+ });
174
+
175
+ this.peripheral.on('read', () => console.log('[FPay BLE Receiver] Read event'));
176
+ this.peripheral.on('subscribe', () => console.log('[FPay BLE Receiver] Central subscribed'));
177
+ this.peripheral.on('unsubscribe', () => console.log('[FPay BLE Receiver] Central unsubscribed'));
178
+ }
179
+
180
+ private _handlePaymentRequest(data: { value: any }): void {
181
+ try {
182
+ let decoded: string;
183
+
184
+ // Handle all value formats the peripheral library may return
185
+ if (typeof data.value === 'string') {
186
+ decoded = Buffer.from(data.value, 'base64').toString('utf8');
187
+ } else if (Buffer.isBuffer(data.value)) {
188
+ decoded = data.value.toString('utf8');
189
+ } else if (Array.isArray(data.value)) {
190
+ decoded = Buffer.from(data.value).toString('utf8');
191
+ } else {
192
+ console.warn('[FPay BLE Receiver] Unknown value format:', typeof data.value);
193
+ return;
194
+ }
195
+
196
+ const request: BLEPaymentRequest = JSON.parse(decoded);
197
+ const deviceId = request.senderId?.toString() ?? 'unknown';
198
+
199
+ console.log('[FPay BLE Receiver] Payment request parsed:', request.senderName, request.currency + request.amount);
200
+
201
+ if (this.onRequestCallback) {
202
+ this.onRequestCallback(request, deviceId);
203
+ } else {
204
+ console.warn('[FPay BLE Receiver] No callback registered — request dropped');
205
+ }
206
+ } catch (err) {
207
+ console.error('[FPay BLE Receiver] Failed to parse payment request:', err);
208
+ }
209
+ }
210
+
211
+ // ── Public API ─────────────────────────────────────────────
212
+
213
+ listenForPaymentRequests(callback: PaymentRequestCallback): void {
214
+ this.onRequestCallback = callback;
215
+ console.log('[FPay BLE Receiver] Payment request listener registered');
216
+ }
217
+
218
+ async sendPaymentResponse(response: BLEPaymentResponse): Promise<void> {
219
+ if (!this.peripheral || !this.isAdvertising) {
220
+ throw new Error('[FPay BLE Receiver] Cannot send response — not advertising');
221
+ }
222
+ const buffer = Buffer.from(JSON.stringify(response));
223
+ await this.peripheral.updateValue(FP_SERVICE_UUID, FP_RESPONSE_CHAR_UUID, buffer);
224
+ console.log('[FPay BLE Receiver] Response sent:', response.accepted ? 'ACCEPTED' : 'DECLINED');
225
+ }
226
+
227
+ async stopAdvertising(): Promise<void> {
228
+ if (this.peripheral && this.isAdvertising) {
229
+ await this.peripheral.stopAdvertising();
230
+ this.isAdvertising = false;
231
+ console.log('[FPay BLE Receiver] Stopped advertising');
232
+ }
233
+ }
234
+
235
+ getIsAdvertising(): boolean { return this.isAdvertising; }
236
+
237
+ destroy(): void {
238
+ this.stopAdvertising();
239
+ this.peripheral = null;
240
+ this.onRequestCallback = null;
241
+ }
242
+ }
243
+
244
+ export default new BLEReceiverService();
@@ -0,0 +1,314 @@
1
+ // FPay SDK — BLESenderService (Central)
2
+ // Ported directly from your BLESenderService.ts.
3
+ // Same UUIDs (imported from BLEReceiverService), same polling strategy,
4
+ // same write fallback (writeWithoutResponse → writeWithResponse).
5
+ // Only changes: renamed types to match SDK conventions, removed Alert imports.
6
+
7
+ import { BleManager, Device, State as BluetoothState, ScanMode } from 'react-native-ble-plx';
8
+ import { PermissionsAndroid, Platform } from 'react-native';
9
+ import { Buffer } from 'buffer';
10
+ import {
11
+ FP_SERVICE_UUID,
12
+ FP_REQUEST_CHAR_UUID,
13
+ FP_RESPONSE_CHAR_UUID,
14
+ FP_USERINFO_CHAR_UUID,
15
+ } from './BLEReceiverService';
16
+ import type { FintechDevice } from '../core/types';
17
+
18
+ // ── Types ─────────────────────────────────────────────────────
19
+
20
+ // A discovered FountainPay-capable BLE device (shown in BluetoothSubScreen list)
21
+
22
+
23
+ // The payment request written to the receiver's characteristic
24
+ export interface BLESendPaymentRequest {
25
+ amount: number;
26
+ currency: string;
27
+ senderId: string;
28
+ senderName: string;
29
+ timestamp: number;
30
+ }
31
+
32
+ // The response polled from the receiver's characteristic
33
+ export interface BLESendPaymentResponse {
34
+ accepted: boolean;
35
+ transactionId?: string;
36
+ timestamp: number;
37
+ // Receiver's bank account — used to process the actual money transfer
38
+ accountDetails?: {
39
+ accountName: string;
40
+ accountNumber: string;
41
+ bankName: string;
42
+ bankCode: string;
43
+ };
44
+ // Echo of the original request.timestamp — used to verify this response
45
+ // belongs to THIS request and not a stale previous one
46
+ requestTimestamp?: number;
47
+ }
48
+
49
+ // ── Class ─────────────────────────────────────────────────────
50
+
51
+ class BLESenderService {
52
+ private bleManager: BleManager;
53
+ private connectedDevices = new Map<string, Device>();
54
+ private monitorSubscription: any = null;
55
+
56
+ constructor() {
57
+ this.bleManager = new BleManager();
58
+ }
59
+
60
+ // ── Init ───────────────────────────────────────────────────
61
+
62
+ async initialize(): Promise<void> {
63
+ await this._requestPermissions();
64
+ await this._initializeBLE();
65
+ }
66
+
67
+ private async _requestPermissions(): Promise<boolean> {
68
+ if (Platform.OS !== 'android') return true;
69
+ if (Platform.Version >= 31) {
70
+ const results = await PermissionsAndroid.requestMultiple([
71
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
72
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
73
+ PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
74
+ ]);
75
+ return Object.values(results).every(r => r === PermissionsAndroid.RESULTS.GRANTED);
76
+ }
77
+ const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION);
78
+ return granted === PermissionsAndroid.RESULTS.GRANTED;
79
+ }
80
+
81
+ private _initializeBLE(): Promise<void> {
82
+ return new Promise((resolve, reject) => {
83
+ const sub = this.bleManager.onStateChange(state => {
84
+ switch (state) {
85
+ case BluetoothState.Unsupported:
86
+ reject(new Error('[FPay BLE Sender] Bluetooth not supported on this device'));
87
+ sub.remove();
88
+ break;
89
+ case BluetoothState.PoweredOff:
90
+ // Attempt to enable — works on Android; iOS will prompt the user
91
+ this.bleManager.enable().catch(reject);
92
+ break;
93
+ case BluetoothState.Unauthorized:
94
+ reject(new Error('[FPay BLE Sender] Bluetooth permission denied'));
95
+ sub.remove();
96
+ break;
97
+ case BluetoothState.PoweredOn:
98
+ console.log('[FPay BLE Sender] Bluetooth ready');
99
+ resolve();
100
+ sub.remove();
101
+ break;
102
+ }
103
+ }, true);
104
+ });
105
+ }
106
+
107
+ // ── Scanning ───────────────────────────────────────────────
108
+
109
+ // Scans for nearby FountainPay BLE peripherals.
110
+ // Connects briefly to each to read the USER_INFO characteristic,
111
+ // then disconnects. Same approach as your BLESenderService.readDeviceInfo().
112
+ async scanForDevices(
113
+ onDeviceFound: (device: FintechDevice) => void,
114
+ durationMs = 5000
115
+ ): Promise<void> {
116
+ const seen = new Set<string>();
117
+
118
+ return new Promise((resolve, reject) => {
119
+ console.log('[FPay BLE Sender] Starting scan...');
120
+
121
+ this.bleManager.startDeviceScan(
122
+ [FP_SERVICE_UUID],
123
+ { allowDuplicates: false, scanMode: ScanMode.LowPower },
124
+ async (error, device) => {
125
+ if (error) {
126
+ console.error('[FPay BLE Sender] Scan error:', error);
127
+ this.bleManager.stopDeviceScan();
128
+ reject(error);
129
+ return;
130
+ }
131
+
132
+ if (device && !seen.has(device.id)) {
133
+ seen.add(device.id);
134
+ console.log('[FPay BLE Sender] Found device:', device.name ?? device.id);
135
+ try {
136
+ const fp = await this._readDeviceInfo(device);
137
+ if (fp) onDeviceFound(fp);
138
+ } catch (err) {
139
+ console.warn('[FPay BLE Sender] Could not read device info:', err);
140
+ }
141
+ }
142
+ }
143
+ );
144
+
145
+ setTimeout(() => {
146
+ this.bleManager.stopDeviceScan();
147
+ console.log('[FPay BLE Sender] Scan complete. Found:', seen.size, 'FPay device(s)');
148
+ resolve();
149
+ }, durationMs);
150
+ });
151
+ }
152
+
153
+ private async _readDeviceInfo(device: Device): Promise<FintechDevice | null> {
154
+ try {
155
+ const connected = await this.bleManager.connectToDevice(device.id);
156
+ await connected.discoverAllServicesAndCharacteristics();
157
+ const char = await connected.readCharacteristicForService(FP_SERVICE_UUID, FP_USERINFO_CHAR_UUID);
158
+ await this.bleManager.cancelDeviceConnection(device.id);
159
+
160
+ if (!char.value) return null;
161
+ const info = JSON.parse(Buffer.from(char.value, 'base64').toString('utf8'));
162
+ return {
163
+ id: device.id,
164
+ name: device.name ?? 'Unknown Device',
165
+ appName: info.appName ?? 'FountainPay',
166
+ userName: info.userName ?? '',
167
+ rssi: device.rssi ?? -100,
168
+ rawDevice: device,
169
+ };
170
+ } catch (err) {
171
+ try { await this.bleManager.cancelDeviceConnection(device.id); } catch {}
172
+ return null;
173
+ }
174
+ }
175
+
176
+ // ── Connection ─────────────────────────────────────────────
177
+
178
+ async connectToDevice(deviceId: string): Promise<Device> {
179
+ console.log('[FPay BLE Sender] Connecting to', deviceId);
180
+ const device = await this.bleManager.connectToDevice(deviceId, { timeout: 15000 });
181
+ await device.discoverAllServicesAndCharacteristics();
182
+ this.connectedDevices.set(deviceId, device);
183
+ console.log('[FPay BLE Sender] Connected and services discovered');
184
+ return device;
185
+ }
186
+
187
+ // ── Send Payment Request ───────────────────────────────────
188
+ //
189
+ // Flow:
190
+ // 1. Connect to receiver device
191
+ // 2. Start polling the RESPONSE characteristic in the background
192
+ // 3. Write the payment request JSON to the REQUEST characteristic
193
+ // (tries writeWithoutResponse first, falls back to writeWithResponse)
194
+ // 4. Wait up to 60s for the receiver to accept/decline
195
+ // The response is matched by requestTimestamp === request.timestamp
196
+ // 5. Return the response (contains accountDetails if accepted)
197
+ //
198
+ // This is your exact polling mechanism, unchanged.
199
+ async sendPaymentRequest(
200
+ deviceId: string,
201
+ request: BLESendPaymentRequest
202
+ ): Promise<BLESendPaymentResponse> {
203
+ let responseReceived = false;
204
+
205
+ try {
206
+ let device = this.connectedDevices.get(deviceId);
207
+ if (!device) device = await this.connectToDevice(deviceId);
208
+
209
+ // Start polling BEFORE writing the request, so we don't miss a fast response
210
+ const responsePromise = new Promise<BLESendPaymentResponse>(async (resolve, reject) => {
211
+ const startTime = Date.now();
212
+
213
+ const poll = async () => {
214
+ while (!responseReceived) {
215
+ if (Date.now() - startTime > 60000) {
216
+ reject(new Error('[FPay BLE Sender] Response timeout — receiver did not respond in 60s'));
217
+ break;
218
+ }
219
+
220
+ try {
221
+ await new Promise(r => setTimeout(r, 1000));
222
+ if (responseReceived) break;
223
+
224
+ console.log('[FPay BLE Sender] Polling response characteristic...');
225
+ const char = await device!.readCharacteristicForService(
226
+ FP_SERVICE_UUID, FP_RESPONSE_CHAR_UUID
227
+ );
228
+
229
+ if (char.value && char.value.length > 0) {
230
+ const decoded = Buffer.from(char.value, 'base64').toString('utf8');
231
+ const resp: BLESendPaymentResponse = JSON.parse(decoded);
232
+
233
+ // Verify this response matches OUR request via the echoed timestamp
234
+ if (
235
+ resp.timestamp &&
236
+ resp.requestTimestamp &&
237
+ resp.requestTimestamp === request.timestamp
238
+ ) {
239
+ console.log('[FPay BLE Sender] Response received — accepted:', resp.accepted);
240
+ responseReceived = true;
241
+ resolve(resp);
242
+ break;
243
+ } else {
244
+ console.log('[FPay BLE Sender] Old/unrelated response, still polling...');
245
+ }
246
+ }
247
+ } catch (pollErr: any) {
248
+ // errorCode 205 = device disconnected — abort immediately
249
+ if (pollErr.errorCode === 205) {
250
+ reject(new Error('[FPay BLE Sender] Device disconnected'));
251
+ break;
252
+ }
253
+ // All other errors: log and keep polling
254
+ console.warn('[FPay BLE Sender] Poll error (non-fatal):', pollErr.message);
255
+ }
256
+ }
257
+ };
258
+
259
+ poll().catch(reject);
260
+ });
261
+
262
+ // Write the payment request
263
+ const base64 = Buffer.from(JSON.stringify(request)).toString('base64');
264
+ console.log('[FPay BLE Sender] Writing payment request...');
265
+
266
+ try {
267
+ await device.writeCharacteristicWithoutResponseForService(
268
+ FP_SERVICE_UUID, FP_REQUEST_CHAR_UUID, base64
269
+ );
270
+ console.log('[FPay BLE Sender] Written without response (preferred)');
271
+ } catch {
272
+ console.log('[FPay BLE Sender] Retrying with response...');
273
+ await device.writeCharacteristicWithResponseForService(
274
+ FP_SERVICE_UUID, FP_REQUEST_CHAR_UUID, base64
275
+ );
276
+ console.log('[FPay BLE Sender] Written with response');
277
+ }
278
+
279
+ console.log('[FPay BLE Sender] Request sent. Waiting for accept/decline...');
280
+
281
+ const response = await responsePromise;
282
+ await this.disconnectDevice(deviceId);
283
+ return response;
284
+ } catch (err) {
285
+ await this.disconnectDevice(deviceId);
286
+ throw err;
287
+ }
288
+ }
289
+
290
+ // ── Disconnect ─────────────────────────────────────────────
291
+
292
+ async disconnectDevice(deviceId: string): Promise<void> {
293
+ if (this.monitorSubscription) {
294
+ try { this.monitorSubscription.remove(); } catch {}
295
+ this.monitorSubscription = null;
296
+ }
297
+ if (this.connectedDevices.has(deviceId)) {
298
+ try { await this.bleManager.cancelDeviceConnection(deviceId); } catch {}
299
+ this.connectedDevices.delete(deviceId);
300
+ console.log('[FPay BLE Sender] Disconnected from', deviceId);
301
+ }
302
+ }
303
+
304
+ // ── Cleanup ────────────────────────────────────────────────
305
+
306
+ destroy(): void {
307
+ this.bleManager.stopDeviceScan();
308
+ this.connectedDevices.clear();
309
+ if (this.monitorSubscription) { try { this.monitorSubscription.remove(); } catch {} }
310
+ this.bleManager.destroy();
311
+ }
312
+ }
313
+
314
+ export default new BLESenderService();