react-native-fpay 0.4.14 → 0.4.16
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/lib/module/FountainPayProvider.js +103 -12
- package/lib/module/FountainPayProvider.js.map +1 -1
- package/lib/module/core/api/client.js +1 -1
- package/lib/module/core/api/client.js.map +1 -1
- package/lib/module/core/api/index.js +2 -1
- package/lib/module/core/api/index.js.map +1 -1
- package/lib/module/core/types/index.js.map +1 -1
- package/lib/module/engine/BLESenderService.js +32 -7
- package/lib/module/engine/BLESenderService.js.map +1 -1
- package/lib/module/engine/FPEngine.js +17 -0
- package/lib/module/engine/FPEngine.js.map +1 -1
- package/lib/module/ui/components/Gradients/Skeleton.js +54 -0
- package/lib/module/ui/components/Gradients/Skeleton.js.map +1 -0
- package/lib/module/ui/components/Gradients/index.js +85 -0
- package/lib/module/ui/components/Gradients/index.js.map +1 -0
- package/lib/module/ui/screens/ResultScreen.js +18 -5
- package/lib/module/ui/screens/ResultScreen.js.map +1 -1
- package/lib/module/ui/screens/sub/sendPayment/BluetoothSubScreen.js +10 -9
- package/lib/module/ui/screens/sub/sendPayment/BluetoothSubScreen.js.map +1 -1
- package/lib/module/ui/screens/sub/sendPayment/ProximitySubScreen.js +3 -7
- package/lib/module/ui/screens/sub/sendPayment/ProximitySubScreen.js.map +1 -1
- package/lib/typescript/src/FountainPayProvider.d.ts +3 -2
- package/lib/typescript/src/FountainPayProvider.d.ts.map +1 -1
- package/lib/typescript/src/core/api/index.d.ts +16 -0
- package/lib/typescript/src/core/api/index.d.ts.map +1 -1
- package/lib/typescript/src/core/types/index.d.ts +2 -0
- package/lib/typescript/src/core/types/index.d.ts.map +1 -1
- package/lib/typescript/src/engine/BLESenderService.d.ts.map +1 -1
- package/lib/typescript/src/engine/FPEngine.d.ts +1 -0
- package/lib/typescript/src/engine/FPEngine.d.ts.map +1 -1
- package/lib/typescript/src/ui/components/Gradients/Skeleton.d.ts +3 -0
- package/lib/typescript/src/ui/components/Gradients/Skeleton.d.ts.map +1 -0
- package/lib/typescript/src/ui/components/Gradients/index.d.ts +3 -0
- package/lib/typescript/src/ui/components/Gradients/index.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/ui/screens/sub/sendPayment/BluetoothSubScreen.d.ts.map +1 -1
- package/lib/typescript/src/ui/screens/sub/sendPayment/ProximitySubScreen.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/FountainPayProvider.tsx +124 -17
- package/src/core/api/client.ts +1 -1
- package/src/core/api/index.ts +18 -0
- package/src/core/types/index.ts +2 -0
- package/src/engine/BLESenderService.ts +44 -17
- package/src/engine/FPEngine.ts +16 -0
- package/src/ui/components/Gradients/Skeleton.tsx +48 -0
- package/src/ui/components/Gradients/index.tsx +94 -0
- package/src/ui/screens/ResultScreen.tsx +23 -6
- package/src/ui/screens/sub/sendPayment/BluetoothSubScreen.tsx +10 -9
- package/src/ui/screens/sub/sendPayment/ProximitySubScreen.tsx +3 -2
|
@@ -63,7 +63,8 @@ export function useFountainPay(): FPInstance {
|
|
|
63
63
|
// ── Provider ──────────────────────────────────────────────────────────────────
|
|
64
64
|
|
|
65
65
|
interface ProviderProps {
|
|
66
|
-
apiKey
|
|
66
|
+
apiKey?: string;
|
|
67
|
+
accessToken?: string;
|
|
67
68
|
options?: FPSDKOptions;
|
|
68
69
|
children: ReactNode;
|
|
69
70
|
onAuthReady?: () => void;
|
|
@@ -72,6 +73,7 @@ interface ProviderProps {
|
|
|
72
73
|
|
|
73
74
|
export function FountainPayProvider({
|
|
74
75
|
apiKey,
|
|
76
|
+
accessToken,
|
|
75
77
|
options,
|
|
76
78
|
children,
|
|
77
79
|
onAuthReady,
|
|
@@ -87,6 +89,9 @@ export function FountainPayProvider({
|
|
|
87
89
|
const account = useFPStore((s) => s.account);
|
|
88
90
|
const user = useFPStore((s) => s.user);
|
|
89
91
|
const balance = useFPStore((s) => s.balance);
|
|
92
|
+
// const resetToken = useFPStore((s) => s.clearAccessToken);
|
|
93
|
+
// const clearUser = useFPStore((s) => s.clearUser);
|
|
94
|
+
|
|
90
95
|
|
|
91
96
|
|
|
92
97
|
useEffect(() => {
|
|
@@ -95,18 +100,96 @@ export function FountainPayProvider({
|
|
|
95
100
|
// Make sure client is initialized before any API call
|
|
96
101
|
console.log('[FountainPay] Provider mounted. HTTP client ready.', options);
|
|
97
102
|
if (!clientBooted.current) {
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
// For AGENT mode — store the token before booting the client
|
|
104
|
+
// so the request interceptor picks it up immediately
|
|
105
|
+
if (options?.userType === 'AGENT' && accessToken) {
|
|
106
|
+
getFPStore().setAccessToken(accessToken);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
initClient(apiKey ?? '', {
|
|
110
|
+
baseUrl: options?.baseUrl,
|
|
100
111
|
environment: options?.environment,
|
|
101
112
|
});
|
|
113
|
+
|
|
102
114
|
clientBooted.current = true;
|
|
103
|
-
console.log(
|
|
104
|
-
'[FountainPay] Provider mounted. HTTP client ready.: ',
|
|
105
|
-
options
|
|
106
|
-
);
|
|
115
|
+
console.log('[FountainPay] HTTP client ready. Mode:', options?.userType ?? 'USER');
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
const authenticate = async () => {
|
|
119
|
+
// await resetToken();
|
|
120
|
+
// await clearUser();
|
|
121
|
+
const userType = options?.userType ?? 'USER';
|
|
122
|
+
|
|
123
|
+
// ── AGENT mode ────────────────────────────────────────
|
|
124
|
+
if (userType === 'AGENT') {
|
|
125
|
+
if (!accessToken) {
|
|
126
|
+
onAuthError?.('accessToken is required for AGENT mode');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
console.log('[FountainPay] Agent mode — validating token...');
|
|
132
|
+
|
|
133
|
+
// Store the token first so the HTTP interceptor picks it up
|
|
134
|
+
getFPStore().setAccessToken(accessToken);
|
|
135
|
+
|
|
136
|
+
// Fetch agent profile from backend
|
|
137
|
+
const response = await authenticateAPI.validateToken();
|
|
138
|
+
if (cancelled) return;
|
|
139
|
+
|
|
140
|
+
if (response.status) {
|
|
141
|
+
const agent = response.payload;
|
|
142
|
+
|
|
143
|
+
// Build FPUserInfo from agent model
|
|
144
|
+
const user: FPUserInfo = {
|
|
145
|
+
firstName: agent.firstName,
|
|
146
|
+
lastName: agent.lastName,
|
|
147
|
+
email: agent.email,
|
|
148
|
+
phone: agent.phone ?? '',
|
|
149
|
+
bvn: agent.bvn,
|
|
150
|
+
userId: agent.id,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Build FPAccount from agent model
|
|
154
|
+
const account: FPAccount = {
|
|
155
|
+
id: agent.id,
|
|
156
|
+
firstName: agent.firstName,
|
|
157
|
+
lastName: agent.lastName,
|
|
158
|
+
phone: agent.phone,
|
|
159
|
+
accountName: `${agent.firstName} ${agent.lastName}`.toUpperCase(),
|
|
160
|
+
accountNumber: agent.accountNumber,
|
|
161
|
+
bankName: agent.bankName,
|
|
162
|
+
bankCode: agent.bankCode, // populate if backend returns it
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Store everything — no need to call _resolveAccount
|
|
166
|
+
getFPStore().setUser(user);
|
|
167
|
+
getFPStore().setPsspId(agent.id);
|
|
168
|
+
getFPStore().setAccount(account);
|
|
169
|
+
|
|
170
|
+
const banks: any = await transferAPI.getBanks();
|
|
171
|
+
if (banks.status) {
|
|
172
|
+
getFPStore().setBanks(banks.payload);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setAuthenticated(true);
|
|
176
|
+
console.log('[FountainPay] Agent validated:', agent.firstName, agent.lastName);
|
|
177
|
+
onAuthReady?.();
|
|
178
|
+
} else {
|
|
179
|
+
getFPStore().clearAccessToken();
|
|
180
|
+
setAuthenticated(false, response.message);
|
|
181
|
+
onAuthError?.(response.message);
|
|
182
|
+
}
|
|
183
|
+
} catch (err: any) {
|
|
184
|
+
if (cancelled) return;
|
|
185
|
+
getFPStore().clearAccessToken();
|
|
186
|
+
setAuthenticated(false, err.message);
|
|
187
|
+
onAuthError?.(err.message);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return; // ← don't fall through to PSSP auth
|
|
191
|
+
}
|
|
192
|
+
|
|
110
193
|
if (isTokenValid()) {
|
|
111
194
|
console.log('[FountainPay] Valid token found — skipping auth.');
|
|
112
195
|
setAuthenticated(true);
|
|
@@ -116,7 +199,7 @@ export function FountainPayProvider({
|
|
|
116
199
|
|
|
117
200
|
try {
|
|
118
201
|
console.log('[FountainPay] Verifying API key...');
|
|
119
|
-
const response = await authenticateAPI.login(apiKey);
|
|
202
|
+
const response = await authenticateAPI.login(apiKey || "");
|
|
120
203
|
if (cancelled) return;
|
|
121
204
|
|
|
122
205
|
if (response.status) {
|
|
@@ -152,14 +235,35 @@ export function FountainPayProvider({
|
|
|
152
235
|
return () => {
|
|
153
236
|
cancelled = true;
|
|
154
237
|
};
|
|
155
|
-
}, [apiKey]);
|
|
238
|
+
}, [apiKey, accessToken]);
|
|
239
|
+
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (options?.userType !== 'AGENT') return;
|
|
242
|
+
if (!isAuthenticated) return;
|
|
243
|
+
|
|
244
|
+
const autoInitialize = async () => {
|
|
245
|
+
const store = getFPStore();
|
|
246
|
+
if (!store.user || !store.account) return;
|
|
247
|
+
|
|
248
|
+
console.log('[FountainPay] Agent mode — auto-initializing from stored data...');
|
|
249
|
+
await FPEngine.initialize(
|
|
250
|
+
accessToken ?? '',
|
|
251
|
+
store.user,
|
|
252
|
+
options ?? {},
|
|
253
|
+
{} // empty callbacks — host app sets these via initializeSDK if they want
|
|
254
|
+
);
|
|
255
|
+
console.log('[FountainPay] Agent auto-initialized.');
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
autoInitialize();
|
|
259
|
+
}, [isAuthenticated, options?.userType]);
|
|
156
260
|
|
|
157
261
|
// Build the instance once per provider lifetime.
|
|
158
262
|
// All components that call useFountainPay() share this exact object.
|
|
159
263
|
const instance = useMemo<FPInstance>(() => {
|
|
160
264
|
console.log(
|
|
161
265
|
'[FountainPay] Creating shared FPInstance for apiKey:',
|
|
162
|
-
apiKey.slice(0, 8) + '...'
|
|
266
|
+
apiKey?.toString().slice(0, 8) + '...'
|
|
163
267
|
);
|
|
164
268
|
|
|
165
269
|
return {
|
|
@@ -175,14 +279,17 @@ export function FountainPayProvider({
|
|
|
175
279
|
'[FountainPay] initializeSDK() called for user:',
|
|
176
280
|
user.firstName
|
|
177
281
|
);
|
|
282
|
+
|
|
283
|
+
if (options?.userType === 'AGENT') {
|
|
284
|
+
if (callbacks) {
|
|
285
|
+
FPEngine.updateCallbacks(callbacks); // just register callbacks
|
|
286
|
+
}
|
|
287
|
+
console.log('[FountainPay] Agent already initialized — callbacks updated.');
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
178
290
|
try {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// user,
|
|
182
|
-
// options ?? {},
|
|
183
|
-
// callbacks ?? {}
|
|
184
|
-
// );
|
|
185
|
-
await FPEngine.initialize(apiKey, user, options ?? {}, {
|
|
291
|
+
|
|
292
|
+
await FPEngine.initialize(apiKey || '', user, options ?? {}, {
|
|
186
293
|
...callbacks,
|
|
187
294
|
onPaymentSent: async (tx) => {
|
|
188
295
|
callbacks?.onPaymentSent?.(tx);
|
package/src/core/api/client.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { FPError } from '../types';
|
|
|
7
7
|
import { getFPStore } from '../../store/FPStore';
|
|
8
8
|
|
|
9
9
|
const DEFAULT_BASE_URL =
|
|
10
|
-
'https://kenisha-happiest-nan.ngrok-free.dev/sdk/v1/fpip/';
|
|
10
|
+
'https://kenisha-happiest-nan.ngrok-free.dev/modal/sdk/v1/fpip/';
|
|
11
11
|
|
|
12
12
|
let _client: AxiosInstance | null = null;
|
|
13
13
|
let _isRefreshing = false;
|
package/src/core/api/index.ts
CHANGED
|
@@ -41,6 +41,24 @@ export const authenticateAPI = {
|
|
|
41
41
|
http()
|
|
42
42
|
.post<{ Response: any }>('/verify-otp', { otp, email })
|
|
43
43
|
.then((r) => r.data),
|
|
44
|
+
|
|
45
|
+
validateToken: () =>
|
|
46
|
+
http().get<{
|
|
47
|
+
status: boolean;
|
|
48
|
+
message: string;
|
|
49
|
+
payload: {
|
|
50
|
+
id: string;
|
|
51
|
+
firstName: string;
|
|
52
|
+
lastName: string;
|
|
53
|
+
email: string;
|
|
54
|
+
phone: string;
|
|
55
|
+
bvn?: string;
|
|
56
|
+
accountNumber: string;
|
|
57
|
+
bankName: string;
|
|
58
|
+
bankCode?: string;
|
|
59
|
+
tradeName?: string;
|
|
60
|
+
};
|
|
61
|
+
}>('/auth/agent/validate-token').then(r => r.data),
|
|
44
62
|
};
|
|
45
63
|
|
|
46
64
|
export const accountAPI = {
|
package/src/core/types/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface FPSDKOptions {
|
|
|
28
28
|
environment?: 'sandbox' | 'production';
|
|
29
29
|
proximityRadius?: number;
|
|
30
30
|
bluetoothDisplayName?: string;
|
|
31
|
+
userType?: 'USER' | 'AGENT';
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export interface FPTransferRecipient {
|
|
@@ -43,6 +44,7 @@ export interface FPWalletTransferRecipient {
|
|
|
43
44
|
accountName: string;
|
|
44
45
|
agentId?: string;
|
|
45
46
|
userId?: string;
|
|
47
|
+
terminalId?: string;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
export interface FPNfcRecipient {
|
|
@@ -195,9 +195,10 @@ class BLESenderService {
|
|
|
195
195
|
rawDevice: device,
|
|
196
196
|
};
|
|
197
197
|
} catch (err) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
console.log("Read Device Info Error: ", err)
|
|
199
|
+
// try {
|
|
200
|
+
// await this.bleManager.cancelDeviceConnection(device.id);
|
|
201
|
+
// } catch {}
|
|
201
202
|
return null;
|
|
202
203
|
}
|
|
203
204
|
}
|
|
@@ -233,10 +234,14 @@ class BLESenderService {
|
|
|
233
234
|
): Promise<BLESendPaymentResponse> {
|
|
234
235
|
let responseReceived = false;
|
|
235
236
|
|
|
237
|
+
|
|
236
238
|
try {
|
|
239
|
+
this.bleManager.stopDeviceScan();
|
|
240
|
+
|
|
237
241
|
let device = this.connectedDevices.get(deviceId);
|
|
238
242
|
if (!device) device = await this.connectToDevice(deviceId);
|
|
239
243
|
|
|
244
|
+
|
|
240
245
|
// Start polling BEFORE writing the request, so we don't miss a fast response
|
|
241
246
|
const responsePromise = new Promise<BLESendPaymentResponse>(
|
|
242
247
|
async (resolve, reject) => {
|
|
@@ -257,6 +262,9 @@ class BLESenderService {
|
|
|
257
262
|
await new Promise((r) => setTimeout(r, 1000));
|
|
258
263
|
if (responseReceived) break;
|
|
259
264
|
|
|
265
|
+
// iOS caches BLE reads — force re-discovery to get fresh values
|
|
266
|
+
|
|
267
|
+
|
|
260
268
|
console.log(
|
|
261
269
|
'[FPay BLE Sender] Polling response characteristic...'
|
|
262
270
|
);
|
|
@@ -292,6 +300,7 @@ class BLESenderService {
|
|
|
292
300
|
}
|
|
293
301
|
} catch (pollErr: any) {
|
|
294
302
|
// errorCode 205 = device disconnected — abort immediately
|
|
303
|
+
console.log('[FPay BLE Sender] Poll error:', pollErr);
|
|
295
304
|
if (pollErr.errorCode === 205) {
|
|
296
305
|
reject(new Error('[FPay BLE Sender] Device disconnected'));
|
|
297
306
|
break;
|
|
@@ -314,22 +323,40 @@ class BLESenderService {
|
|
|
314
323
|
console.log('[FPay BLE Sender] Writing payment request...');
|
|
315
324
|
|
|
316
325
|
try {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
} catch {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
326
|
+
await device.writeCharacteristicWithResponseForService(
|
|
327
|
+
FP_SERVICE_UUID,
|
|
328
|
+
FP_REQUEST_CHAR_UUID,
|
|
329
|
+
base64
|
|
330
|
+
);
|
|
331
|
+
console.log('[FPay BLE Sender] Written with response');
|
|
332
|
+
} catch (e: any) {
|
|
333
|
+
console.log('[FPay BLE Sender] Write with response failed:', e.message);
|
|
334
|
+
console.log('[FPay BLE Sender] Retrying without response...');
|
|
335
|
+
await device.writeCharacteristicWithoutResponseForService(
|
|
336
|
+
FP_SERVICE_UUID,
|
|
337
|
+
FP_REQUEST_CHAR_UUID,
|
|
338
|
+
base64
|
|
339
|
+
);
|
|
340
|
+
console.log('[FPay BLE Sender] Written without response');
|
|
331
341
|
}
|
|
332
342
|
|
|
343
|
+
// try {
|
|
344
|
+
// await device.writeCharacteristicWithoutResponseForService(
|
|
345
|
+
// FP_SERVICE_UUID,
|
|
346
|
+
// FP_REQUEST_CHAR_UUID,
|
|
347
|
+
// base64
|
|
348
|
+
// );
|
|
349
|
+
// console.log('[FPay BLE Sender] Written without response (preferred)');
|
|
350
|
+
// } catch {
|
|
351
|
+
// console.log('[FPay BLE Sender] Retrying with response...');
|
|
352
|
+
// await device.writeCharacteristicWithResponseForService(
|
|
353
|
+
// FP_SERVICE_UUID,
|
|
354
|
+
// FP_REQUEST_CHAR_UUID,
|
|
355
|
+
// base64
|
|
356
|
+
// );
|
|
357
|
+
// console.log('[FPay BLE Sender] Written with response');
|
|
358
|
+
// }
|
|
359
|
+
|
|
333
360
|
console.log(
|
|
334
361
|
'[FPay BLE Sender] Request sent. Waiting for accept/decline...'
|
|
335
362
|
);
|
package/src/engine/FPEngine.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// This replaces AlwaysOnPaymentListener entirely — all its logic lives here.
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
Alert,
|
|
6
7
|
AppState,
|
|
7
8
|
type AppStateStatus,
|
|
8
9
|
Vibration,
|
|
@@ -434,6 +435,16 @@ export const FPEngine = {
|
|
|
434
435
|
_isReady = true;
|
|
435
436
|
console.log('[FPay Engine] Initialized. Advertising as:', displayName);
|
|
436
437
|
|
|
438
|
+
// AGENT — account already stored by provider, skip resolve
|
|
439
|
+
if (options.userType === 'AGENT') {
|
|
440
|
+
const account = getFPStore().account;
|
|
441
|
+
if (account) {
|
|
442
|
+
callbacks.onAccountReady?.(account);
|
|
443
|
+
_fetchBalance();
|
|
444
|
+
}
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
437
448
|
// Resolve account first — sets psspId in store
|
|
438
449
|
// Then broadcast proximity — needs psspId to be set
|
|
439
450
|
await _resolveAccount(user, callbacks);
|
|
@@ -563,6 +574,11 @@ export const FPEngine = {
|
|
|
563
574
|
return BLESenderService.sendPaymentRequest(deviceId, request);
|
|
564
575
|
},
|
|
565
576
|
|
|
577
|
+
async updateCallbacks(callbacks: FPCallbacks): Promise<void> {
|
|
578
|
+
_callbacks = { ..._callbacks, ...callbacks };
|
|
579
|
+
console.log('[FPay Engine] Callbacks updated.');
|
|
580
|
+
},
|
|
581
|
+
|
|
566
582
|
// ── Other SDK actions ──────────────────────────────────────
|
|
567
583
|
|
|
568
584
|
async generateAccount(req: FPUserInfo): Promise<FPAccount> {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React, { useRef, useEffect } from 'react';
|
|
2
|
+
import { Animated, View, StyleSheet } from 'react-native';
|
|
3
|
+
import LinearGradient from 'react-native-linear-gradient';
|
|
4
|
+
|
|
5
|
+
const Skeleton = ({ width, height, borderRadius = 8, style }: any) => {
|
|
6
|
+
const translateX = useRef(new Animated.Value(-100)).current;
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
Animated.loop(
|
|
10
|
+
Animated.timing(translateX, {
|
|
11
|
+
toValue: 300,
|
|
12
|
+
duration: 1200,
|
|
13
|
+
useNativeDriver: true,
|
|
14
|
+
})
|
|
15
|
+
).start();
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<View
|
|
20
|
+
style={[
|
|
21
|
+
{
|
|
22
|
+
width,
|
|
23
|
+
height,
|
|
24
|
+
borderRadius,
|
|
25
|
+
backgroundColor: '#e0e0e0',
|
|
26
|
+
overflow: 'hidden',
|
|
27
|
+
},
|
|
28
|
+
style,
|
|
29
|
+
]}
|
|
30
|
+
>
|
|
31
|
+
<Animated.View
|
|
32
|
+
style={{
|
|
33
|
+
...StyleSheet.absoluteFillObject,
|
|
34
|
+
transform: [{ translateX }],
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
<LinearGradient
|
|
38
|
+
colors={['#e0e0e0', '#f5f5f5', '#e0e0e0']}
|
|
39
|
+
start={{ x: 0, y: 0 }}
|
|
40
|
+
end={{ x: 1, y: 0 }}
|
|
41
|
+
style={{ flex: 1 }}
|
|
42
|
+
/>
|
|
43
|
+
</Animated.View>
|
|
44
|
+
</View>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default Skeleton;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Animated, View } from "react-native";
|
|
2
|
+
import { C, R, S } from "../../theme";
|
|
3
|
+
import Skeleton from "./Skeleton";
|
|
4
|
+
import styled from "styled-components/native";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
const Container = styled(Animated.View)`
|
|
9
|
+
flex: 1;
|
|
10
|
+
background-color: ${C.white};
|
|
11
|
+
padding: ${S.xxl}px ${S.lg}px ${S.xl}px;
|
|
12
|
+
align-items: center;
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
const Card = styled.View`
|
|
17
|
+
width: 100%;
|
|
18
|
+
background-color: ${C.surface};
|
|
19
|
+
border-radius: ${R.xl}px;
|
|
20
|
+
padding: ${S.md}px ${S.lg}px;
|
|
21
|
+
margin-bottom: ${S.xl}px;
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const Footer = styled.View`
|
|
25
|
+
flex-direction: row;
|
|
26
|
+
align-items: center;
|
|
27
|
+
margin-top: auto;
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
const Gradients = () => {
|
|
32
|
+
return (
|
|
33
|
+
<Container>
|
|
34
|
+
{/* Icon */}
|
|
35
|
+
<Skeleton
|
|
36
|
+
width={80}
|
|
37
|
+
height={80}
|
|
38
|
+
borderRadius={40}
|
|
39
|
+
style={{ alignSelf: 'center', marginBottom: 20 }}
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
{/* Status Text */}
|
|
43
|
+
<Skeleton
|
|
44
|
+
width={200}
|
|
45
|
+
height={20}
|
|
46
|
+
style={{ alignSelf: 'center', marginBottom: 16 }}
|
|
47
|
+
/>
|
|
48
|
+
|
|
49
|
+
{/* Amount */}
|
|
50
|
+
<Skeleton
|
|
51
|
+
width={180}
|
|
52
|
+
height={30}
|
|
53
|
+
style={{ alignSelf: 'center', marginBottom: 24 }}
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
{/* Card */}
|
|
57
|
+
<Card>
|
|
58
|
+
{[...Array(5)].map((_, i) => (
|
|
59
|
+
<View
|
|
60
|
+
key={i}
|
|
61
|
+
style={{
|
|
62
|
+
flexDirection: 'row',
|
|
63
|
+
justifyContent: 'space-between',
|
|
64
|
+
marginBottom: 14,
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<Skeleton width={100} height={14} />
|
|
68
|
+
<Skeleton width={140} height={14} />
|
|
69
|
+
</View>
|
|
70
|
+
))}
|
|
71
|
+
</Card>
|
|
72
|
+
|
|
73
|
+
{/* Footer */}
|
|
74
|
+
<Footer style={{ marginTop: 30 }}>
|
|
75
|
+
{/* Countdown ring placeholder */}
|
|
76
|
+
<Skeleton
|
|
77
|
+
width={60}
|
|
78
|
+
height={60}
|
|
79
|
+
borderRadius={30}
|
|
80
|
+
style={{ marginBottom: 20 }}
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
{/* Button */}
|
|
84
|
+
<Skeleton
|
|
85
|
+
width={'100%'}
|
|
86
|
+
height={50}
|
|
87
|
+
borderRadius={25}
|
|
88
|
+
/>
|
|
89
|
+
</Footer>
|
|
90
|
+
</Container>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export default Gradients;
|
|
@@ -5,6 +5,7 @@ import styled from 'styled-components/native';
|
|
|
5
5
|
import { C, F, R, S } from '../theme';
|
|
6
6
|
import type { FPTransaction } from '../../core/types';
|
|
7
7
|
import { transferAPI } from '../../core/api';
|
|
8
|
+
import Gradients from '../components/Gradients';
|
|
8
9
|
|
|
9
10
|
// ── Icons ─────────────────────────────────────────────────────
|
|
10
11
|
|
|
@@ -203,6 +204,7 @@ interface Props {
|
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
export function ResultScreen({ transaction, onClose }: Props) {
|
|
207
|
+
const [loading, setLoading] = useState<boolean>(false);
|
|
206
208
|
const [transactionDetail, setTransactionDetail] = useState<any>(null)
|
|
207
209
|
const slideAnim = useRef(new Animated.Value(60)).current;
|
|
208
210
|
const opacAnim = useRef(new Animated.Value(0)).current;
|
|
@@ -232,11 +234,20 @@ export function ResultScreen({ transaction, onClose }: Props) {
|
|
|
232
234
|
: '—';
|
|
233
235
|
|
|
234
236
|
const loadTransaction = async()=>{
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
setLoading(true);
|
|
238
|
+
setTransactionDetail(null);
|
|
239
|
+
try{
|
|
240
|
+
const response = await transferAPI.status(transaction.reference) as any;
|
|
241
|
+
console.log("Transaction payload: ", response);
|
|
242
|
+
if(response.status){
|
|
243
|
+
setTransactionDetail(response.payload);
|
|
244
|
+
}
|
|
245
|
+
}catch(err: any){
|
|
246
|
+
console.error("Error loading transactions status")
|
|
247
|
+
}finally{
|
|
248
|
+
setLoading(false);
|
|
239
249
|
}
|
|
250
|
+
|
|
240
251
|
}
|
|
241
252
|
|
|
242
253
|
useEffect(() => {
|
|
@@ -256,10 +267,16 @@ export function ResultScreen({ transaction, onClose }: Props) {
|
|
|
256
267
|
if(transaction){
|
|
257
268
|
loadTransaction();
|
|
258
269
|
}
|
|
259
|
-
}, [transaction])
|
|
270
|
+
}, [transaction]);
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if (loading && !transactionDetail) {
|
|
274
|
+
return <Gradients />;
|
|
275
|
+
}
|
|
260
276
|
|
|
261
277
|
return (
|
|
262
278
|
<Container style={{ opacity: opacAnim, transform: [{ translateY: slideAnim }] }}>
|
|
279
|
+
|
|
263
280
|
<IconWrap style={{ transform: [{ scale: scaleAnim }] }}>
|
|
264
281
|
<IconBg bg={bgColor}>
|
|
265
282
|
{isSuccess ? <SuccessIcon /> : <FailedIcon />}
|
|
@@ -282,7 +299,7 @@ export function ResultScreen({ transaction, onClose }: Props) {
|
|
|
282
299
|
|
|
283
300
|
<Footer>
|
|
284
301
|
<CountdownRing duration={COUNTDOWN_SECONDS * 1000} />
|
|
285
|
-
<CloseBtn onPress={
|
|
302
|
+
<CloseBtn onPress={onClose} activeOpacity={0.8}>
|
|
286
303
|
<CloseBtnText>Close</CloseBtnText>
|
|
287
304
|
</CloseBtn>
|
|
288
305
|
</Footer>
|
|
@@ -17,15 +17,9 @@ import styled from 'styled-components/native';
|
|
|
17
17
|
import Svg, { Path } from 'react-native-svg';
|
|
18
18
|
import Ionicons from 'react-native-vector-icons/Ionicons';
|
|
19
19
|
|
|
20
|
-
import { transferAPI } from '../../../../core/api';
|
|
21
|
-
import { FPButton } from '../../../components/FPButton';
|
|
22
|
-
import { C, R, S, F, shadow } from '../../../theme';
|
|
23
20
|
import type {
|
|
24
|
-
FPCurrency,
|
|
25
21
|
FPError,
|
|
26
|
-
FPSendPaymentRequest,
|
|
27
22
|
FPSendWalletPaymentRequest,
|
|
28
|
-
FPTransaction,
|
|
29
23
|
FPUserInfo,
|
|
30
24
|
FintechDevice,
|
|
31
25
|
Props,
|
|
@@ -61,7 +55,7 @@ const Header = styled(View)`
|
|
|
61
55
|
|
|
62
56
|
const HeaderButton = styled(TouchableOpacity)`
|
|
63
57
|
padding: 8px;
|
|
64
|
-
background-color:
|
|
58
|
+
background-color: transparent;
|
|
65
59
|
width: 40px;
|
|
66
60
|
height: 40px;
|
|
67
61
|
border-radius: 50px;
|
|
@@ -79,6 +73,7 @@ const HeaderRight = styled(View)`
|
|
|
79
73
|
flex-direction: row;
|
|
80
74
|
align-items: center;
|
|
81
75
|
gap: 8px;
|
|
76
|
+
width: 25%;
|
|
82
77
|
`;
|
|
83
78
|
|
|
84
79
|
const QuickLinksContainer = styled(View)`
|
|
@@ -343,6 +338,8 @@ export function BluetoothSubScreen({
|
|
|
343
338
|
request
|
|
344
339
|
);
|
|
345
340
|
|
|
341
|
+
await FPEngine.stopListening();
|
|
342
|
+
|
|
346
343
|
console.log('Component response: ', response);
|
|
347
344
|
|
|
348
345
|
if (!response.accepted) {
|
|
@@ -367,8 +364,10 @@ export function BluetoothSubScreen({
|
|
|
367
364
|
recipient: {
|
|
368
365
|
accountName: response.accountDetails.accountName,
|
|
369
366
|
accountNumber: response.accountDetails.accountNumber,
|
|
370
|
-
userId: response.accountDetails.userId,
|
|
367
|
+
userId: response.accountDetails.userId || "",
|
|
368
|
+
agentId: response.accountDetails.agentId || "",
|
|
371
369
|
type: response.accountDetails.receiverType ?? 'USER',
|
|
370
|
+
terminalId: response.accountDetails.terminalId || ''
|
|
372
371
|
},
|
|
373
372
|
amount: request.amount.toString(),
|
|
374
373
|
reference: response.transactionId ?? `BT_${Date.now()}`,
|
|
@@ -402,6 +401,8 @@ export function BluetoothSubScreen({
|
|
|
402
401
|
Alert.alert('Error', 'Failed to send payment request');
|
|
403
402
|
}
|
|
404
403
|
onError?.(fp);
|
|
404
|
+
}finally{
|
|
405
|
+
await FPEngine.startListening();
|
|
405
406
|
}
|
|
406
407
|
};
|
|
407
408
|
|
|
@@ -422,7 +423,7 @@ export function BluetoothSubScreen({
|
|
|
422
423
|
|
|
423
424
|
<HeaderRight>
|
|
424
425
|
<HeaderButton>
|
|
425
|
-
<Ionicons name="ellipsis-vertical" size={22} />
|
|
426
|
+
{/* <Ionicons name="ellipsis-vertical" size={22} /> */}
|
|
426
427
|
</HeaderButton>
|
|
427
428
|
</HeaderRight>
|
|
428
429
|
</Header>
|