react-native-fpay 0.4.14 → 0.4.15
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 +79 -6
- 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 +10 -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 +7 -2
- package/lib/module/ui/screens/sub/sendPayment/BluetoothSubScreen.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.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/package.json +1 -1
- package/src/FountainPayProvider.tsx +89 -11
- 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 +11 -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 +7 -7
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);
|
|
@@ -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,
|
|
@@ -343,6 +337,8 @@ export function BluetoothSubScreen({
|
|
|
343
337
|
request
|
|
344
338
|
);
|
|
345
339
|
|
|
340
|
+
await FPEngine.stopListening();
|
|
341
|
+
|
|
346
342
|
console.log('Component response: ', response);
|
|
347
343
|
|
|
348
344
|
if (!response.accepted) {
|
|
@@ -367,8 +363,10 @@ export function BluetoothSubScreen({
|
|
|
367
363
|
recipient: {
|
|
368
364
|
accountName: response.accountDetails.accountName,
|
|
369
365
|
accountNumber: response.accountDetails.accountNumber,
|
|
370
|
-
userId: response.accountDetails.userId,
|
|
366
|
+
userId: response.accountDetails.userId || "",
|
|
367
|
+
agentId: response.accountDetails.agentId || "",
|
|
371
368
|
type: response.accountDetails.receiverType ?? 'USER',
|
|
369
|
+
terminalId: response.accountDetails.terminalId || ''
|
|
372
370
|
},
|
|
373
371
|
amount: request.amount.toString(),
|
|
374
372
|
reference: response.transactionId ?? `BT_${Date.now()}`,
|
|
@@ -402,6 +400,8 @@ export function BluetoothSubScreen({
|
|
|
402
400
|
Alert.alert('Error', 'Failed to send payment request');
|
|
403
401
|
}
|
|
404
402
|
onError?.(fp);
|
|
403
|
+
}finally{
|
|
404
|
+
await FPEngine.startListening();
|
|
405
405
|
}
|
|
406
406
|
};
|
|
407
407
|
|