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,370 @@
1
+ // FPay SDK — FPEngine
2
+ // Orchestrates BLEReceiverService, BLESenderService, proximity, and the event bus.
3
+ // This replaces AlwaysOnPaymentListener entirely — all its logic lives here.
4
+
5
+ import { AppState, type AppStateStatus, Vibration, Platform } from 'react-native';
6
+ import AsyncStorage from '@react-native-async-storage/async-storage';
7
+ import Geolocation from '@react-native-community/geolocation';
8
+ import { initClient } from '../core/api/client';
9
+ import { proximityAPI, walletAPI } from '../core/api';
10
+ import type {
11
+ FPUserInfo,
12
+ FPSDKOptions,
13
+ FPCallbacks,
14
+ FPBluetoothPaymentRequest,
15
+ FPVirtualAccount,
16
+ FPGenerateAccountRequest,
17
+ FPError,
18
+ FintechDevice,
19
+ } from '../core/types';
20
+ import BLEReceiverService, { type BLEPaymentRequest, type BLEPaymentResponse } from './BLEReceiverService';
21
+ import BLESenderService, { type BLESendPaymentRequest, type BLESendPaymentResponse } from './BLESenderService';
22
+
23
+ // ── Module-level singleton state ──────────────────────────────
24
+ let _apiKey: string | null = null;
25
+ let _user: FPUserInfo | null = null;
26
+ let _callbacks: FPCallbacks = {};
27
+ let _options: FPSDKOptions = {};
28
+ let _isReady = false;
29
+ let _isListening = false;
30
+ let _proximitySessionId: string | null = null;
31
+ let _proximityInterval: ReturnType<typeof setInterval> | null = null;
32
+ let _appStateSub: ReturnType<typeof AppState.addEventListener> | null = null;
33
+
34
+ // ── Internal event bus ────────────────────────────────────────
35
+ // Used to communicate between FPEngine and the UI layer
36
+ // without any React coupling.
37
+
38
+ type FPEventName = 'incoming_payment_request' | 'show_send' | 'show_receive';
39
+ const _listeners = new Map<FPEventName, Set<Function>>();
40
+
41
+ export function _emitEvent(event: FPEventName, data?: unknown): void {
42
+ _listeners.get(event)?.forEach(fn => fn(data));
43
+ }
44
+
45
+ export function _onEvent(event: FPEventName, fn: Function): () => void {
46
+ if (!_listeners.has(event)) _listeners.set(event, new Set());
47
+ _listeners.get(event)!.add(fn);
48
+ return () => _listeners.get(event)?.delete(fn);
49
+ }
50
+
51
+ // ── Proximity helpers ─────────────────────────────────────────
52
+
53
+ function _clearProximityInterval(): void {
54
+ if (_proximityInterval) { clearInterval(_proximityInterval); _proximityInterval = null; }
55
+ }
56
+
57
+ function _getLocation(): Promise<{ lat: number; lng: number }> {
58
+ return new Promise((resolve, reject) => {
59
+ Geolocation.getCurrentPosition(
60
+ p => resolve({ lat: p.coords.latitude, lng: p.coords.longitude }),
61
+ e => reject({ code: 'LOCATION_DENIED', message: e.message } as FPError),
62
+ { enableHighAccuracy: true, timeout: 8000, maximumAge: 10000 }
63
+ );
64
+ });
65
+ }
66
+
67
+ async function _startProximityBroadcast(): Promise<void> {
68
+ if (!_user) return;
69
+ try {
70
+ const loc = await _getLocation();
71
+ const session = await proximityAPI.broadcast({
72
+ latitude: loc.lat, longitude: loc.lng,
73
+ accountName: _user.accountName,
74
+ accountNumber: _user.accountNumber,
75
+ bankCode: _user.bankCode,
76
+ bankName: _user.bankName,
77
+ });
78
+ _proximitySessionId = session.sessionId;
79
+ _clearProximityInterval();
80
+ _proximityInterval = setInterval(async () => {
81
+ if (!_proximitySessionId) return;
82
+ try {
83
+ const l = await _getLocation();
84
+ await proximityAPI.heartbeat(_proximitySessionId, l.lat, l.lng);
85
+ } catch { /* silent — heartbeat failure is non-fatal */ }
86
+ }, 15000);
87
+ console.log('[FPay Engine] Proximity broadcast active');
88
+ } catch (err) {
89
+ // Non-fatal — app still works without proximity
90
+ console.warn('[FPay Engine] Proximity unavailable:', (err as FPError).message);
91
+ }
92
+ }
93
+
94
+ async function _stopProximityBroadcast(): Promise<void> {
95
+ _clearProximityInterval();
96
+ if (_proximitySessionId) {
97
+ await proximityAPI.unregister(_proximitySessionId).catch(() => {});
98
+ _proximitySessionId = null;
99
+ }
100
+ }
101
+
102
+ // ── Security validation ───────────────────────────────────────
103
+ // Ported from AlwaysOnPaymentListener.validatePaymentRequest()
104
+
105
+ async function _validateRequest(req: BLEPaymentRequest): Promise<boolean> {
106
+ // 1. Age check — reject requests older than 5 minutes
107
+ const AGE_LIMIT = 5 * 60 * 1000;
108
+ if (Date.now() - req.timestamp > AGE_LIMIT) {
109
+ console.log('[FPay Engine] Rejected: request expired');
110
+ return false;
111
+ }
112
+
113
+ // 2. Blocked sender check
114
+ try {
115
+ const raw = await AsyncStorage.getItem('@fp_blocked_users');
116
+ const blocked: string[] = raw ? JSON.parse(raw) : [];
117
+ if (blocked.includes(req.senderId)) {
118
+ console.log('[FPay Engine] Rejected: sender is blocked');
119
+ return false;
120
+ }
121
+ } catch {}
122
+
123
+ // 3. Amount sanity (must be positive; max 10,000,000 kobo = NGN 100,000)
124
+ if (req.amount <= 0 || req.amount > 10000000) {
125
+ console.log('[FPay Engine] Rejected: invalid amount', req.amount);
126
+ return false;
127
+ }
128
+
129
+ // 4. Duplicate detection — prevents replay attacks
130
+ try {
131
+ const raw = await AsyncStorage.getItem('@fp_recent_requests');
132
+ const recent: BLEPaymentRequest[] = raw ? JSON.parse(raw) : [];
133
+ const isDuplicate = recent.some(r =>
134
+ r.senderId === req.senderId &&
135
+ r.amount === req.amount &&
136
+ Math.abs(r.timestamp - req.timestamp) < 60000 // within 1 minute
137
+ );
138
+ // Store and trim to last 50
139
+ await AsyncStorage.setItem(
140
+ '@fp_recent_requests',
141
+ JSON.stringify([...recent, req].slice(-50))
142
+ );
143
+ if (isDuplicate) {
144
+ console.log('[FPay Engine] Rejected: duplicate request');
145
+ return false;
146
+ }
147
+ } catch {}
148
+
149
+ return true;
150
+ }
151
+
152
+ // ── Incoming BLE payment request handler ─────────────────────
153
+ // This is the core of AlwaysOnPaymentListener.handleIncomingPaymentRequest()
154
+ // It fires automatically when BLEReceiverService receives a write event.
155
+
156
+ async function _handleIncomingRequest(req: BLEPaymentRequest, deviceId: string): Promise<void> {
157
+ console.log('[FPay Engine] Incoming payment request from:', req.senderName, req.currency + req.amount);
158
+
159
+ const isValid = await _validateRequest(req);
160
+ if (!isValid) {
161
+ // Auto-decline invalid requests silently
162
+ await BLEReceiverService.sendPaymentResponse({ accepted: false, timestamp: Date.now() }).catch(() => {});
163
+ return;
164
+ }
165
+
166
+ // Vibrate to alert user — same pattern as your original code
167
+ Vibration.vibrate([0, 200, 100, 200]);
168
+
169
+ // Map BLEPaymentRequest → FPBluetoothPaymentRequest (the SDK's public type)
170
+ const fpRequest: FPBluetoothPaymentRequest = {
171
+ requestId: req.senderId + '_' + req.timestamp,
172
+ sender: {
173
+ accountName: req.senderName,
174
+ deviceId,
175
+ deviceName: req.senderName,
176
+ },
177
+ amount: req.amount,
178
+ currency: req.currency,
179
+ timestamp: new Date(req.timestamp).toISOString(),
180
+ };
181
+
182
+ // 1. Fire host app callback (optional — host may want to log, update state, etc.)
183
+ _callbacks.onPaymentRequest?.(fpRequest);
184
+
185
+ // 2. Fire internal event → FPPaymentRequestModal will render the Accept/Decline UI
186
+ _emitEvent('incoming_payment_request', fpRequest);
187
+ }
188
+
189
+ // ── FPEngine public API ───────────────────────────────────────
190
+
191
+ export const FPEngine = {
192
+
193
+ // Called by pay.initializeSDK()
194
+ // Mirrors AlwaysOnPaymentListener.initializeAlwaysOnService()
195
+ async initialize(
196
+ apiKey: string,
197
+ user: FPUserInfo,
198
+ options: FPSDKOptions = {},
199
+ callbacks: FPCallbacks = {}
200
+ ): Promise<void> {
201
+ _apiKey = apiKey;
202
+ _user = user;
203
+ _options = options;
204
+ _callbacks = callbacks;
205
+
206
+ // Boot the HTTP client
207
+ initClient(apiKey, { baseUrl: options.baseUrl, environment: options.environment });
208
+
209
+ // Initialize BLE receiver with this user's info
210
+ const displayName = options.bluetoothDisplayName ?? user.accountName;
211
+ await BLEReceiverService.initializeWithUserInfo(
212
+ user.userId ?? user.accountNumber,
213
+ displayName,
214
+ 'FountainPay'
215
+ );
216
+
217
+ // Start BLE advertising + proximity broadcast in parallel.
218
+ // Both are non-fatal — SDK still works if either fails.
219
+ await Promise.allSettled([
220
+ BLEReceiverService.startAdvertising(),
221
+ _startProximityBroadcast(),
222
+ ]);
223
+
224
+ // Initialize BLE sender (central) — used when user sends payments via BT
225
+ await BLESenderService.initialize().catch(err =>
226
+ console.warn('[FPay Engine] BLE sender init failed (non-fatal):', err)
227
+ );
228
+
229
+ // Resume broadcasting when app returns to foreground
230
+ _appStateSub = AppState.addEventListener('change', (next: AppStateStatus) => {
231
+ if (next === 'active' && _isReady) {
232
+ _startProximityBroadcast();
233
+ BLEReceiverService.startAdvertising().catch(() => {});
234
+ } else if (next === 'background') {
235
+ _clearProximityInterval();
236
+ }
237
+ });
238
+
239
+ _isReady = true;
240
+ console.log('[FPay Engine] Initialized. Advertising as:', displayName);
241
+ },
242
+
243
+ async deviceConnection (deviceId: string): Promise<void>{
244
+ await BLESenderService.connectToDevice(deviceId);
245
+ },
246
+
247
+ // Called by pay.listen()
248
+ // Registers the BLE write callback — same as your:
249
+ // BLEReceiverService.listenForPaymentRequests(handleIncomingPaymentRequest)
250
+ startListening(): void {
251
+ if (_isListening) return;
252
+ _isListening = true;
253
+ BLEReceiverService.listenForPaymentRequests(_handleIncomingRequest);
254
+ console.log('[FPay Engine] Listening for BLE payment requests');
255
+ },
256
+
257
+ stopListening(): void {
258
+ _isListening = false;
259
+ // Replace with a no-op so the peripheral write handler doesn't crash
260
+ BLEReceiverService.listenForPaymentRequests(() => {});
261
+ },
262
+
263
+ // Called by FPPaymentRequestModal when user taps "Accept"
264
+ // Mirrors AlwaysOnPaymentListener.handleAcceptPayment()
265
+ async acceptPaymentRequest(request: FPBluetoothPaymentRequest): Promise<void> {
266
+ try {
267
+ const transactionId = 'txn_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
268
+
269
+ // Build response — same shape as your original:
270
+ // { accepted, transactionId, timestamp, accountDetails, requestTimestamp }
271
+ const response: BLEPaymentResponse = {
272
+ accepted: true,
273
+ transactionId,
274
+ timestamp: Date.now(),
275
+ accountDetails: _user ? {
276
+ accountName: _user.accountName,
277
+ accountNumber: _user.accountNumber,
278
+ bankName: _user.bankName,
279
+ bankCode: _user.bankCode,
280
+ } : undefined,
281
+ // requestTimestamp is echoed back so the sender's polling can match it
282
+ requestTimestamp: new Date(request.timestamp).getTime(),
283
+ };
284
+
285
+ // Write response to the BLE characteristic — sender is polling this
286
+ await BLEReceiverService.sendPaymentResponse(response);
287
+ console.log('[FPay Engine] Acceptance response sent to sender');
288
+
289
+ // Notify host app
290
+ const tx = {
291
+ id: transactionId,
292
+ reference: transactionId,
293
+ type: 'credit' as const,
294
+ channel: 'bluetooth' as const,
295
+ amount: request.amount,
296
+ currency: request.currency,
297
+ status: 'successful' as const,
298
+ sender: request.sender,
299
+ createdAt: new Date().toISOString(),
300
+ };
301
+ _callbacks.onPaymentReceived?.(tx);
302
+ } catch (err) {
303
+ _callbacks.onError?.(err as FPError);
304
+ }
305
+ },
306
+
307
+ // Called by FPPaymentRequestModal when user taps "Decline"
308
+ // Mirrors AlwaysOnPaymentListener.handleDeclinePayment()
309
+ async declinePaymentRequest(request: FPBluetoothPaymentRequest): Promise<void> {
310
+ try {
311
+ await BLEReceiverService.sendPaymentResponse({ accepted: false, timestamp: Date.now() });
312
+ console.log('[FPay Engine] Decline response sent to sender');
313
+ } catch {
314
+ // Non-fatal — decline even if response send fails
315
+ }
316
+ _callbacks.onPaymentDeclined?.(request);
317
+ },
318
+
319
+ // ── Used by BluetoothSubScreen (Send side) ─────────────────
320
+
321
+ // Scan for nearby FPay BLE devices
322
+ async scanForBluetoothDevices(
323
+ onDeviceFound: (device: FintechDevice) => void,
324
+ durationMs = 7000
325
+ ): Promise<void> {
326
+ return BLESenderService.scanForDevices(onDeviceFound, durationMs);
327
+ },
328
+
329
+ // Send a BLE payment request to a discovered device.
330
+ // Returns the response which contains the receiver's accountDetails
331
+ // if accepted — the calling screen then calls transferAPI to move the money.
332
+ async sendBluetoothPaymentRequest(
333
+ deviceId: string,
334
+ request: BLESendPaymentRequest
335
+ ): Promise<BLESendPaymentResponse> {
336
+ return BLESenderService.sendPaymentRequest(deviceId, request);
337
+ },
338
+
339
+ // ── Other SDK actions ──────────────────────────────────────
340
+
341
+ async generateAccount(req: FPGenerateAccountRequest): Promise<FPVirtualAccount> {
342
+ return walletAPI.generate(req);
343
+ },
344
+
345
+ showSend(amount: number, currency: string): void {
346
+ _emitEvent('show_send', { amount, currency });
347
+ },
348
+
349
+ showReceive(amount?: number, currency?: string): void {
350
+ _emitEvent('show_receive', { amount, currency });
351
+ },
352
+
353
+ getUser: () => _user,
354
+ isReady: () => _isReady,
355
+ getCallbacks: () => _callbacks,
356
+
357
+ async destroy(): Promise<void> {
358
+ _clearProximityInterval();
359
+ await _stopProximityBroadcast();
360
+ await BLEReceiverService.stopAdvertising();
361
+ BLESenderService.destroy();
362
+ _isListening = false;
363
+ _appStateSub?.remove();
364
+ _isReady = false;
365
+ _user = null;
366
+ _proximitySessionId = null;
367
+ _listeners.clear();
368
+ console.log('[FPay Engine] Destroyed');
369
+ },
370
+ };
@@ -0,0 +1,106 @@
1
+ // NearbyUsersService.ts
2
+ import Geolocation from '@react-native-community/geolocation';
3
+
4
+ interface NearbyUser {
5
+ userId: string;
6
+ userName: string;
7
+ distance: number; // meters
8
+ lastSeen: number; // timestamp
9
+ }
10
+
11
+ class NearbyUsersService {
12
+ private locationUpdateInterval: any;
13
+ private readonly API_URL = 'https://api.tapit.app/v1/near-by-users';
14
+
15
+ // Start broadcasting your location
16
+ async startBroadcasting() {
17
+ // Update location every 5 seconds
18
+ this.locationUpdateInterval = setInterval(() => {
19
+ this.sendLocationToServer();
20
+ }, 5000);
21
+ }
22
+
23
+ private async sendLocationToServer() {
24
+ Geolocation.getCurrentPosition(
25
+ async (position) => {
26
+ const { latitude, longitude } = position.coords;
27
+
28
+ const authToken = await this.getAuthToken();
29
+
30
+ await fetch(`${this.API_URL}/api/users/location`, {
31
+ method: 'POST',
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ 'Authorization': `Bearer ${authToken}`,
35
+ },
36
+ body: JSON.stringify({
37
+ latitude,
38
+ longitude,
39
+ timestamp: Date.now(),
40
+ }),
41
+ });
42
+ },
43
+ (error) => console.error('Location error:', error),
44
+ { enableHighAccuracy: true, timeout: 20000, maximumAge: 1000 }
45
+ );
46
+ }
47
+
48
+ // Get list of nearby users from server
49
+ async getNearbyUsers(): Promise<NearbyUser[]> {
50
+ const authToken = await this.getAuthToken();
51
+
52
+ const response = await fetch(`${this.API_URL}/api/users/nearby?radius=50`, {
53
+ headers: {
54
+ 'Authorization': `Bearer ${authToken}`,
55
+ },
56
+ });
57
+
58
+ if (!response.ok) {
59
+ throw new Error('Failed to get nearby users');
60
+ }
61
+
62
+ return response.json();
63
+ }
64
+
65
+ // Send payment request through server
66
+ async sendPaymentRequest(
67
+ receiverId: string,
68
+ amount: number,
69
+ currency: string = 'USD'
70
+ ) {
71
+ const authToken = await this.getAuthToken();
72
+
73
+ const response = await fetch(`${this.API_URL}/api/payments/request`, {
74
+ method: 'POST',
75
+ headers: {
76
+ 'Content-Type': 'application/json',
77
+ 'Authorization': `Bearer ${authToken}`,
78
+ },
79
+ body: JSON.stringify({
80
+ receiverId,
81
+ amount,
82
+ currency,
83
+ timestamp: Date.now(),
84
+ }),
85
+ });
86
+
87
+ if (!response.ok) {
88
+ throw new Error('Failed to send payment request');
89
+ }
90
+
91
+ return response.json();
92
+ }
93
+
94
+ stopBroadcasting() {
95
+ if (this.locationUpdateInterval) {
96
+ clearInterval(this.locationUpdateInterval);
97
+ }
98
+ }
99
+
100
+ private async getAuthToken(): Promise<string> {
101
+ // Get from your auth system
102
+ return 'your_jwt_token';
103
+ }
104
+ }
105
+
106
+ export default new NearbyUsersService();
package/src/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ // ─────────────────────────────────────────────
2
+ // react-native-fountainpay-sdk
3
+ // Public API — this is everything the host app
4
+ // needs to import. Nothing else.
5
+ // ─────────────────────────────────────────────
6
+
7
+ // The one hook
8
+ export { useFountainPay } from './useFountainPay';
9
+
10
+ // The one provider (mount at app root)
11
+ export { FountainPayProvider } from './FountainPayProvider';
12
+
13
+ // All TypeScript types (for type-safe usage)
14
+ export type {
15
+ FPInstance,
16
+ FPUserInfo,
17
+ FPSDKOptions,
18
+ FPCallbacks,
19
+ FPCurrency,
20
+ FPTransaction,
21
+ FPVirtualAccount,
22
+ FPGenerateAccountRequest,
23
+ FPBluetoothPaymentRequest,
24
+ FPProximityPeer,
25
+ FPNQRData,
26
+ FPBankItem,
27
+ FPError,
28
+ FPTxStatus,
29
+ FPChannel,
30
+ } from './core/types';
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import { TouchableOpacity, Text, ActivityIndicator, StyleSheet, type ViewStyle } from 'react-native';
3
+ import { C, R, F } from '../theme';
4
+
5
+ interface Props {
6
+ label: string;
7
+ onPress: () => void;
8
+ variant?: 'primary' | 'outline' | 'ghost' | 'danger';
9
+ loading?: boolean;
10
+ disabled?: boolean;
11
+ style?: ViewStyle;
12
+ }
13
+
14
+ export function FPButton({ label, onPress, variant = 'primary', loading, disabled, style }: Props) {
15
+ return (
16
+ <TouchableOpacity
17
+ style={[styles.base, styles[variant], (disabled || loading) && styles.disabled, style]}
18
+ onPress={onPress}
19
+ disabled={disabled || loading}
20
+ activeOpacity={0.75}
21
+ >
22
+ {loading
23
+ ? <ActivityIndicator color={variant === 'primary' || variant === 'danger' ? C.white : C.ink} size="small" />
24
+ : <Text style={[styles.label, styles[variant + 'Label' as keyof typeof styles]]}>{label}</Text>
25
+ }
26
+ </TouchableOpacity>
27
+ );
28
+ }
29
+
30
+ const styles = StyleSheet.create({
31
+ base: { height: 52, borderRadius: R.md, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 20 },
32
+ primary: { backgroundColor: C.brand },
33
+ outline: { backgroundColor: 'transparent', borderWidth: 1.5, borderColor: C.brand },
34
+ ghost: { backgroundColor: 'transparent' },
35
+ danger: { backgroundColor: C.red },
36
+ disabled: { opacity: 0.45 },
37
+ label: { fontSize: F.md, fontWeight: '700' },
38
+ primaryLabel: { color: C.white },
39
+ outlineLabel: { color: C.brand },
40
+ ghostLabel: { color: C.ink },
41
+ dangerLabel: { color: C.white },
42
+ });
@@ -0,0 +1,88 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { Modal } from 'react-native';
3
+ import styled from 'styled-components/native';
4
+ import { Animated, Easing } from "react-native";
5
+ import Svg, { Circle, G } from "react-native-svg";
6
+
7
+
8
+ type Props = {
9
+ size?: number,
10
+ text?: string;
11
+ };
12
+
13
+ const Overlay = styled.View`
14
+ position: absolute;
15
+ top: 0;
16
+ right: 0;
17
+ bottom: 0;
18
+ left: 0;
19
+
20
+ align-items: center;
21
+ justify-content: center;
22
+
23
+ background-color: rgba(100, 100, 100, 0.2);
24
+ `;
25
+
26
+ const IndicatorContainer = styled.View`
27
+ padding: 12px;
28
+ background-color: #ffffff;
29
+ border-radius: 12px;
30
+ align-items: center;
31
+ `;
32
+
33
+ const IndicatorText = styled.Text`
34
+ font-size: 18px;
35
+ margin-top: 12px;
36
+ color: #003333;
37
+ `;
38
+
39
+
40
+ const AnimatedSvg = Animated.createAnimatedComponent(Svg);
41
+
42
+ const InLoading = ({ size=40, text = 'Loading...' }: Props) => {
43
+ const [color, setColor] = useState<'teal' | 'royalblue'>('teal');
44
+
45
+ const rotate = useRef(new Animated.Value(0)).current;
46
+
47
+ useEffect(() => {
48
+ Animated.loop(
49
+ Animated.timing(rotate, {
50
+ toValue: 1,
51
+ duration: 800,
52
+ easing: Easing.linear,
53
+ useNativeDriver: true,
54
+ })
55
+ ).start();
56
+ }, [rotate]);
57
+
58
+ const spin = rotate.interpolate({
59
+ inputRange: [0, 1],
60
+ outputRange: ["0deg", "360deg"],
61
+ });
62
+
63
+
64
+
65
+ return (
66
+ <AnimatedSvg
67
+ width={size}
68
+ height={size}
69
+ viewBox="0 0 100 100"
70
+ style={{ transform: [{ rotate: spin }] }}
71
+ >
72
+ <G>
73
+ <Circle
74
+ cx="50"
75
+ cy="50"
76
+ r="33"
77
+ stroke="#0a3d2e"
78
+ strokeWidth="4"
79
+ strokeLinecap="round"
80
+ strokeDasharray="52 52"
81
+ fill="none"
82
+ />
83
+ </G>
84
+ </AnimatedSvg>
85
+ );
86
+ };
87
+
88
+ export default InLoading;