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,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();
|