react-native-fpay 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Fpay.podspec +20 -0
- package/LICENSE +20 -0
- package/README.md +37 -0
- package/android/build.gradle +67 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/fpay/FpayModule.kt +15 -0
- package/android/src/main/java/com/fpay/FpayPackage.kt +31 -0
- package/ios/Fpay.h +5 -0
- package/ios/Fpay.mm +21 -0
- package/lib/module/FountainPayProvider.js +18 -0
- package/lib/module/FountainPayProvider.js.map +1 -0
- package/lib/module/core/api/client.js +47 -0
- package/lib/module/core/api/client.js.map +1 -0
- package/lib/module/core/api/index.js +35 -0
- package/lib/module/core/api/index.js.map +1 -0
- package/lib/module/core/types/index.js +4 -0
- package/lib/module/core/types/index.js.map +1 -0
- package/lib/module/engine/BLEReceiverService.js +190 -0
- package/lib/module/engine/BLEReceiverService.js.map +1 -0
- package/lib/module/engine/BLESenderService.js +259 -0
- package/lib/module/engine/BLESenderService.js.map +1 -0
- package/lib/module/engine/FPEngine.js +340 -0
- package/lib/module/engine/FPEngine.js.map +1 -0
- package/lib/module/engine/NearbyUsersService.js +87 -0
- package/lib/module/engine/NearbyUsersService.js.map +1 -0
- package/lib/module/index.js +16 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/ui/components/FPButton.js +71 -0
- package/lib/module/ui/components/FPButton.js.map +1 -0
- package/lib/module/ui/components/LoadingAnimation/InLoading.js +74 -0
- package/lib/module/ui/components/LoadingAnimation/InLoading.js.map +1 -0
- package/lib/module/ui/components/LoadingAnimation/index.js +82 -0
- package/lib/module/ui/components/LoadingAnimation/index.js.map +1 -0
- package/lib/module/ui/components/OtpInput/OTPInputView.js +290 -0
- package/lib/module/ui/components/OtpInput/OTPInputView.js.map +1 -0
- package/lib/module/ui/components/OtpInput/Styles.js +20 -0
- package/lib/module/ui/components/OtpInput/Styles.js.map +1 -0
- package/lib/module/ui/components/OtpInput/helpers/codeToArray.js +7 -0
- package/lib/module/ui/components/OtpInput/helpers/codeToArray.js.map +1 -0
- package/lib/module/ui/components/OtpInput/helpers/device.js +9 -0
- package/lib/module/ui/components/OtpInput/helpers/device.js.map +1 -0
- package/lib/module/ui/components/OtpInput/helpers/styles.js +17 -0
- package/lib/module/ui/components/OtpInput/helpers/styles.js.map +1 -0
- package/lib/module/ui/components/OtpInput/helpers/types.js +4 -0
- package/lib/module/ui/components/OtpInput/helpers/types.js.map +1 -0
- package/lib/module/ui/components/OtpInput/index.js +45 -0
- package/lib/module/ui/components/OtpInput/index.js.map +1 -0
- package/lib/module/ui/components/PulseAnimation.js +61 -0
- package/lib/module/ui/components/PulseAnimation.js.map +1 -0
- package/lib/module/ui/modals/FPPaymentRequestModal.js +253 -0
- package/lib/module/ui/modals/FPPaymentRequestModal.js.map +1 -0
- package/lib/module/ui/modals/FPShell.js +180 -0
- package/lib/module/ui/modals/FPShell.js.map +1 -0
- package/lib/module/ui/screens/ReceiveScreen.js +291 -0
- package/lib/module/ui/screens/ReceiveScreen.js.map +1 -0
- package/lib/module/ui/screens/SendScreen.js +216 -0
- package/lib/module/ui/screens/SendScreen.js.map +1 -0
- package/lib/module/ui/screens/sub/BluetoothSubScreen.js +403 -0
- package/lib/module/ui/screens/sub/BluetoothSubScreen.js.map +1 -0
- package/lib/module/ui/screens/sub/NFCSubScreen.js +169 -0
- package/lib/module/ui/screens/sub/NFCSubScreen.js.map +1 -0
- package/lib/module/ui/screens/sub/NQRSubScreen.js +136 -0
- package/lib/module/ui/screens/sub/NQRSubScreen.js.map +1 -0
- package/lib/module/ui/screens/sub/ProximitySubScreen.js +501 -0
- package/lib/module/ui/screens/sub/ProximitySubScreen.js.map +1 -0
- package/lib/module/ui/screens/sub/TransferSubScreen.js +361 -0
- package/lib/module/ui/screens/sub/TransferSubScreen.js.map +1 -0
- package/lib/module/ui/theme/index.js +64 -0
- package/lib/module/ui/theme/index.js.map +1 -0
- package/lib/module/useFountainPay.js +82 -0
- package/lib/module/useFountainPay.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/FountainPayProvider.d.ts +7 -0
- package/lib/typescript/src/FountainPayProvider.d.ts.map +1 -0
- package/lib/typescript/src/core/api/client.d.ts +7 -0
- package/lib/typescript/src/core/api/client.d.ts.map +1 -0
- package/lib/typescript/src/core/api/index.d.ts +67 -0
- package/lib/typescript/src/core/api/index.d.ts.map +1 -0
- package/lib/typescript/src/core/types/index.d.ts +130 -0
- package/lib/typescript/src/core/types/index.d.ts.map +1 -0
- package/lib/typescript/src/engine/BLEReceiverService.d.ts +43 -0
- package/lib/typescript/src/engine/BLEReceiverService.d.ts.map +1 -0
- package/lib/typescript/src/engine/BLESenderService.d.ts +39 -0
- package/lib/typescript/src/engine/BLESenderService.d.ts.map +1 -0
- package/lib/typescript/src/engine/FPEngine.d.ts +24 -0
- package/lib/typescript/src/engine/FPEngine.d.ts.map +1 -0
- package/lib/typescript/src/engine/NearbyUsersService.d.ts +19 -0
- package/lib/typescript/src/engine/NearbyUsersService.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/FPButton.d.ts +12 -0
- package/lib/typescript/src/ui/components/FPButton.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/LoadingAnimation/InLoading.d.ts +7 -0
- package/lib/typescript/src/ui/components/LoadingAnimation/InLoading.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/LoadingAnimation/index.d.ts +6 -0
- package/lib/typescript/src/ui/components/LoadingAnimation/index.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/OtpInput/OTPInputView.d.ts +29 -0
- package/lib/typescript/src/ui/components/OtpInput/OTPInputView.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/OtpInput/Styles.d.ts +330 -0
- package/lib/typescript/src/ui/components/OtpInput/Styles.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/OtpInput/helpers/codeToArray.d.ts +6 -0
- package/lib/typescript/src/ui/components/OtpInput/helpers/codeToArray.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/OtpInput/helpers/device.d.ts +6 -0
- package/lib/typescript/src/ui/components/OtpInput/helpers/device.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/OtpInput/helpers/styles.d.ts +6 -0
- package/lib/typescript/src/ui/components/OtpInput/helpers/styles.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/OtpInput/helpers/types.d.ts +84 -0
- package/lib/typescript/src/ui/components/OtpInput/helpers/types.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/OtpInput/index.d.ts +9 -0
- package/lib/typescript/src/ui/components/OtpInput/index.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/PulseAnimation.d.ts +2 -0
- package/lib/typescript/src/ui/components/PulseAnimation.d.ts.map +1 -0
- package/lib/typescript/src/ui/modals/FPPaymentRequestModal.d.ts +2 -0
- package/lib/typescript/src/ui/modals/FPPaymentRequestModal.d.ts.map +1 -0
- package/lib/typescript/src/ui/modals/FPShell.d.ts +2 -0
- package/lib/typescript/src/ui/modals/FPShell.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/ReceiveScreen.d.ts +10 -0
- package/lib/typescript/src/ui/screens/ReceiveScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/SendScreen.d.ts +9 -0
- package/lib/typescript/src/ui/screens/SendScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/sub/BluetoothSubScreen.d.ts +552 -0
- package/lib/typescript/src/ui/screens/sub/BluetoothSubScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/sub/NFCSubScreen.d.ts +19 -0
- package/lib/typescript/src/ui/screens/sub/NFCSubScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/sub/NQRSubScreen.d.ts +13 -0
- package/lib/typescript/src/ui/screens/sub/NQRSubScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/sub/ProximitySubScreen.d.ts +552 -0
- package/lib/typescript/src/ui/screens/sub/ProximitySubScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/sub/TransferSubScreen.d.ts +12 -0
- package/lib/typescript/src/ui/screens/sub/TransferSubScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/theme/index.d.ts +62 -0
- package/lib/typescript/src/ui/theme/index.d.ts.map +1 -0
- package/lib/typescript/src/useFountainPay.d.ts +3 -0
- package/lib/typescript/src/useFountainPay.d.ts.map +1 -0
- package/package.json +217 -0
- package/src/FountainPayProvider.tsx +21 -0
- package/src/core/api/client.ts +47 -0
- package/src/core/api/index.ts +61 -0
- package/src/core/types/index.ts +144 -0
- package/src/engine/BLEReceiverService.ts +244 -0
- package/src/engine/BLESenderService.ts +314 -0
- package/src/engine/FPEngine.ts +370 -0
- package/src/engine/NearbyUsersService.ts +106 -0
- package/src/index.ts +30 -0
- package/src/ui/components/FPButton.tsx +42 -0
- package/src/ui/components/LoadingAnimation/InLoading.tsx +88 -0
- package/src/ui/components/LoadingAnimation/index.tsx +93 -0
- package/src/ui/components/OtpInput/OTPInputView.tsx +243 -0
- package/src/ui/components/OtpInput/Styles.ts +19 -0
- package/src/ui/components/OtpInput/helpers/codeToArray.ts +3 -0
- package/src/ui/components/OtpInput/helpers/device.ts +6 -0
- package/src/ui/components/OtpInput/helpers/styles.ts +17 -0
- package/src/ui/components/OtpInput/helpers/types.ts +88 -0
- package/src/ui/components/OtpInput/index.tsx +51 -0
- package/src/ui/components/PulseAnimation.tsx +78 -0
- package/src/ui/modals/FPPaymentRequestModal.tsx +158 -0
- package/src/ui/modals/FPShell.tsx +107 -0
- package/src/ui/screens/ReceiveScreen.tsx +119 -0
- package/src/ui/screens/SendScreen.tsx +86 -0
- package/src/ui/screens/sub/BluetoothSubScreen.tsx +433 -0
- package/src/ui/screens/sub/NFCSubScreen.tsx +83 -0
- package/src/ui/screens/sub/NQRSubScreen.tsx +61 -0
- package/src/ui/screens/sub/ProximitySubScreen.tsx +390 -0
- package/src/ui/screens/sub/TransferSubScreen.tsx +146 -0
- package/src/ui/theme/index.ts +24 -0
- package/src/useFountainPay.ts +95 -0
|
@@ -0,0 +1,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;
|