react-native-fpay 0.4.19 → 0.4.21
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 +50 -126
- package/lib/module/FountainPayProvider.js.map +1 -1
- package/lib/module/core/api/client.js +1 -1
- package/lib/module/core/api/index.js +16 -12
- package/lib/module/core/api/index.js.map +1 -1
- package/lib/module/core/types/index.js +1 -1
- package/lib/module/core/types/index.js.map +1 -1
- package/lib/module/engine/FPEngine.js +27 -16
- package/lib/module/engine/FPEngine.js.map +1 -1
- package/lib/module/store/FPStore.js +11 -4
- package/lib/module/store/FPStore.js.map +1 -1
- package/lib/module/ui/components/ConfirmScreen.js +2 -2
- package/lib/module/ui/components/ConfirmScreen.js.map +1 -1
- package/lib/module/ui/modals/FPCreatePin.js +234 -0
- package/lib/module/ui/modals/FPCreatePin.js.map +1 -0
- package/lib/module/ui/modals/FPShell.js +7 -3
- package/lib/module/ui/modals/FPShell.js.map +1 -1
- package/lib/module/ui/modals/FPValidateBvn.js +258 -0
- package/lib/module/ui/modals/FPValidateBvn.js.map +1 -0
- package/lib/module/ui/screens/ResultScreen.js +4 -17
- package/lib/module/ui/screens/ResultScreen.js.map +1 -1
- package/lib/module/ui/screens/SendScreen.js +24 -9
- package/lib/module/ui/screens/SendScreen.js.map +1 -1
- package/lib/module/ui/screens/sub/sendPayment/ProximitySubScreen.js +3 -3
- package/lib/module/ui/screens/sub/sendPayment/ProximitySubScreen.js.map +1 -1
- package/lib/module/ui/screens/sub/sendPayment/TransferSubScreen.js +8 -1
- package/lib/module/ui/screens/sub/sendPayment/TransferSubScreen.js.map +1 -1
- package/lib/typescript/src/FountainPayProvider.d.ts +3 -4
- package/lib/typescript/src/FountainPayProvider.d.ts.map +1 -1
- package/lib/typescript/src/core/api/index.d.ts +20 -20
- package/lib/typescript/src/core/api/index.d.ts.map +1 -1
- package/lib/typescript/src/core/types/index.d.ts +16 -6
- package/lib/typescript/src/core/types/index.d.ts.map +1 -1
- package/lib/typescript/src/engine/FPEngine.d.ts +3 -0
- package/lib/typescript/src/engine/FPEngine.d.ts.map +1 -1
- package/lib/typescript/src/store/FPStore.d.ts +8 -5
- package/lib/typescript/src/store/FPStore.d.ts.map +1 -1
- package/lib/typescript/src/ui/components/ConfirmScreen.d.ts +1 -2
- package/lib/typescript/src/ui/components/ConfirmScreen.d.ts.map +1 -1
- package/lib/typescript/src/ui/modals/FPCreatePin.d.ts +3 -0
- package/lib/typescript/src/ui/modals/FPCreatePin.d.ts.map +1 -0
- package/lib/typescript/src/ui/modals/FPShell.d.ts.map +1 -1
- package/lib/typescript/src/ui/modals/FPValidateBvn.d.ts +3 -0
- package/lib/typescript/src/ui/modals/FPValidateBvn.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/ui/screens/SendScreen.d.ts.map +1 -1
- package/lib/typescript/src/ui/screens/sub/sendPayment/TransferSubScreen.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/FountainPayProvider.tsx +59 -149
- package/src/core/api/client.ts +1 -1
- package/src/core/api/index.ts +56 -52
- package/src/core/types/index.ts +18 -5
- package/src/engine/FPEngine.ts +34 -17
- package/src/store/FPStore.ts +15 -8
- package/src/ui/components/ConfirmScreen.tsx +4 -3
- package/src/ui/modals/FPCreatePin.tsx +206 -0
- package/src/ui/modals/FPShell.tsx +16 -2
- package/src/ui/modals/FPValidateBvn.tsx +229 -0
- package/src/ui/screens/ResultScreen.tsx +6 -10
- package/src/ui/screens/SendScreen.tsx +60 -36
- package/src/ui/screens/sub/sendPayment/ProximitySubScreen.tsx +3 -3
- package/src/ui/screens/sub/sendPayment/TransferSubScreen.tsx +7 -1
package/src/store/FPStore.ts
CHANGED
|
@@ -30,7 +30,8 @@ export interface FPStoreState {
|
|
|
30
30
|
tokenExpiresAt: number | null; // unix ms — null means no expiry info
|
|
31
31
|
|
|
32
32
|
// User set via initializeSDK()
|
|
33
|
-
user:
|
|
33
|
+
user: any | null;
|
|
34
|
+
tempId: string;
|
|
34
35
|
|
|
35
36
|
// Account details set via setAccountDetails()
|
|
36
37
|
accountDetails: FPTransferRecipient | null;
|
|
@@ -50,7 +51,7 @@ export interface FPStoreState {
|
|
|
50
51
|
|
|
51
52
|
lastBroadcastPosition: { latitude: number; longitude: number } | null;
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
appId: string | null;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
// ── Actions shape ─────────────────────────────────────────────────────────────
|
|
@@ -59,8 +60,11 @@ export interface FPStoreActions {
|
|
|
59
60
|
setAccessToken: (token: string, expiresInSeconds?: number) => void;
|
|
60
61
|
clearAccessToken: () => void;
|
|
61
62
|
|
|
62
|
-
setUser: (user:
|
|
63
|
+
setUser: (user: any) => void;
|
|
63
64
|
clearUser: () => void;
|
|
65
|
+
|
|
66
|
+
setTempId: (temp: string) => void;
|
|
67
|
+
clearTempId: () => void;
|
|
64
68
|
|
|
65
69
|
setAccountDetails: (account: FPTransferRecipient) => void;
|
|
66
70
|
setBalance: (payload: FPBalance) => void;
|
|
@@ -80,7 +84,7 @@ export interface FPStoreActions {
|
|
|
80
84
|
// Full reset — call on logout
|
|
81
85
|
reset: () => void;
|
|
82
86
|
|
|
83
|
-
|
|
87
|
+
setAppId: (id: string) => void;
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
export type FPStore = FPStoreState & FPStoreActions;
|
|
@@ -101,7 +105,8 @@ const initialState: FPStoreState = {
|
|
|
101
105
|
psspId: null,
|
|
102
106
|
account: null,
|
|
103
107
|
lastBroadcastPosition: null,
|
|
104
|
-
|
|
108
|
+
appId: null,
|
|
109
|
+
tempId: ''
|
|
105
110
|
};
|
|
106
111
|
|
|
107
112
|
// ── Store ─────────────────────────────────────────────────────────────────────
|
|
@@ -126,9 +131,11 @@ export const useFPStore = create<FPStore>()(
|
|
|
126
131
|
}),
|
|
127
132
|
|
|
128
133
|
setUser: (user) => set({ user }),
|
|
129
|
-
|
|
130
134
|
clearUser: () => set({ user: null }),
|
|
131
135
|
|
|
136
|
+
setTempId: (temp) => set({ tempId: temp }),
|
|
137
|
+
clearTempId: () => set({ tempId: "" }),
|
|
138
|
+
|
|
132
139
|
setAccountDetails: (accountDetails) => set({ accountDetails }),
|
|
133
140
|
setBalance: (payload) => set({ balance: payload, balanceFetchedAt: Date.now() }),
|
|
134
141
|
clearBalance: () => set({ balance: null, balanceFetchedAt: null }),
|
|
@@ -162,7 +169,7 @@ export const useFPStore = create<FPStore>()(
|
|
|
162
169
|
|
|
163
170
|
reset: () => set(initialState),
|
|
164
171
|
|
|
165
|
-
|
|
172
|
+
setAppId: (id: string) => set({ appId: id }),
|
|
166
173
|
}),
|
|
167
174
|
{
|
|
168
175
|
name: '@fp_store', // AsyncStorage key
|
|
@@ -180,7 +187,7 @@ export const useFPStore = create<FPStore>()(
|
|
|
180
187
|
psspId: state.psspId,
|
|
181
188
|
account: state.account,
|
|
182
189
|
balance: state.balance,
|
|
183
|
-
|
|
190
|
+
appId: state.appId,
|
|
184
191
|
balanceFetchedAt: state.balanceFetchedAt,
|
|
185
192
|
lastBroadcastPosition: state.lastBroadcastPosition,
|
|
186
193
|
// isAuthenticated and authError are NOT persisted —
|
|
@@ -15,6 +15,7 @@ import styled from 'styled-components/native';
|
|
|
15
15
|
import OTPInputs from './OtpInput';
|
|
16
16
|
import InLoading from './LoadingAnimation/InLoading';
|
|
17
17
|
import type {
|
|
18
|
+
FPInternalTransferRecipient,
|
|
18
19
|
FPSendPaymentRequest,
|
|
19
20
|
FPSendWalletPaymentRequest,
|
|
20
21
|
FPTransferRecipient,
|
|
@@ -181,7 +182,7 @@ const ProcessingRow = styled.View`
|
|
|
181
182
|
|
|
182
183
|
interface TransferReminderScreenProps {
|
|
183
184
|
userId: string;
|
|
184
|
-
transaction:
|
|
185
|
+
transaction: any;
|
|
185
186
|
onContinue: (tempId: string) => void;
|
|
186
187
|
}
|
|
187
188
|
|
|
@@ -266,13 +267,13 @@ const ConfirmScreen: React.FC<TransferReminderScreenProps> = ({
|
|
|
266
267
|
const recipientName = transaction?.recipient.accountName ?? '';
|
|
267
268
|
const accountNumber = transaction?.recipient.accountNumber ?? '';
|
|
268
269
|
const bankName = !isWalletTransfer
|
|
269
|
-
? (transaction?.recipient as
|
|
270
|
+
? (transaction?.recipient as any)?.bankName
|
|
270
271
|
: undefined;
|
|
271
272
|
|
|
272
273
|
const handleContinue = async () => {
|
|
273
274
|
setProcessing(true);
|
|
274
275
|
try {
|
|
275
|
-
const response: any = await transferAPI.validateTransfer(otp, userId);
|
|
276
|
+
const response: any = await transferAPI.validateTransfer(otp, userId, transaction?.recipient.id);
|
|
276
277
|
console.log('Validate: ', response);
|
|
277
278
|
if (!response.status) {
|
|
278
279
|
Vibration.vibrate(100);
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Animated, Modal, StyleSheet, Text, TouchableOpacity, View } from "react-native"
|
|
4
|
+
|
|
5
|
+
import { C, R, S, F, shadow } from '../theme';
|
|
6
|
+
import OTPInputs from "../components/OtpInput";
|
|
7
|
+
import styled from "styled-components/native";
|
|
8
|
+
import InLoading from "../components/LoadingAnimation/InLoading";
|
|
9
|
+
import { FPEngine } from "../../engine/FPEngine";
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const ProcessingRow = styled.View`
|
|
15
|
+
width: 100%;
|
|
16
|
+
display: flex;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
align-items: center;
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const ButtonRow = styled(View)`
|
|
22
|
+
flex-direction: row;
|
|
23
|
+
gap: 12px;
|
|
24
|
+
margin-top: 32px;
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
const ContinueButton = styled(TouchableOpacity)`
|
|
28
|
+
flex: 1;
|
|
29
|
+
background-color: #003333;
|
|
30
|
+
border-radius: 50px;
|
|
31
|
+
padding-vertical: 17px;
|
|
32
|
+
align-items: center;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
shadow-color: #fff;
|
|
35
|
+
shadow-offset: 0px 4px;
|
|
36
|
+
shadow-opacity: 0.3;
|
|
37
|
+
shadow-radius: 10px;
|
|
38
|
+
elevation: 6;
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
const ContinueButtonText = styled(Text)`
|
|
42
|
+
color: #ffffff;
|
|
43
|
+
font-size: 16px;
|
|
44
|
+
font-weight: 700;
|
|
45
|
+
letter-spacing: -0.2px;
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
const SubText = styled(Text)`
|
|
49
|
+
width: 90%;
|
|
50
|
+
font-size: 11px;
|
|
51
|
+
color: #666;
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const Container = styled(View)`
|
|
55
|
+
width: 100%;
|
|
56
|
+
display: flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
gap: 10px;
|
|
60
|
+
`
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
const FPCreatePin = () => {
|
|
64
|
+
const [processing, setProcessing] = useState(false);
|
|
65
|
+
const [otp, setOtp] = useState<string>('');
|
|
66
|
+
const scale = React.useRef(new Animated.Value(0.85)).current;
|
|
67
|
+
const buttonAnim = React.useRef(new Animated.Value(0)).current;
|
|
68
|
+
const [error, setError] = useState("")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
const handleCreatePin = async() => {
|
|
72
|
+
setError("");
|
|
73
|
+
setProcessing(true);
|
|
74
|
+
try{
|
|
75
|
+
await FPEngine.createUserTransactionPin({pin: otp});
|
|
76
|
+
} catch (error: any) {
|
|
77
|
+
console.error('Error creating PIN:', error);
|
|
78
|
+
setError(error.message || "Error creating PIN")
|
|
79
|
+
} finally {
|
|
80
|
+
setProcessing(false);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
React.useEffect(() => {
|
|
86
|
+
Animated.sequence([
|
|
87
|
+
Animated.timing(buttonAnim, {
|
|
88
|
+
toValue: 1,
|
|
89
|
+
duration: 300,
|
|
90
|
+
useNativeDriver: true,
|
|
91
|
+
}),
|
|
92
|
+
]).start();
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<Modal
|
|
97
|
+
visible
|
|
98
|
+
transparent
|
|
99
|
+
animationType="slide"
|
|
100
|
+
statusBarTranslucent
|
|
101
|
+
>
|
|
102
|
+
<View
|
|
103
|
+
style={{
|
|
104
|
+
flex: 1,
|
|
105
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
106
|
+
justifyContent: 'center',
|
|
107
|
+
alignItems: 'center',
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
<Animated.View style={{ transform: [{ scale }] }}>
|
|
111
|
+
<View
|
|
112
|
+
style={[st.card, {
|
|
113
|
+
borderRadius: 20,
|
|
114
|
+
padding: 30,
|
|
115
|
+
}]}
|
|
116
|
+
>
|
|
117
|
+
<Container>
|
|
118
|
+
<Text style={{ color: C.brand, fontSize: 16, fontWeight: '600', marginBottom: 16 }}>
|
|
119
|
+
Create PIN
|
|
120
|
+
</Text>
|
|
121
|
+
<OTPInputs count={4} callBack={setOtp} />
|
|
122
|
+
<SubText>Create your preferred pin to authorize transactions</SubText>
|
|
123
|
+
{error && <Text style={{ color: 'red', fontSize: 12, marginTop: 6 }}>{error}</Text>}
|
|
124
|
+
</Container>
|
|
125
|
+
|
|
126
|
+
{processing ? (
|
|
127
|
+
<ProcessingRow>
|
|
128
|
+
<ButtonRow>
|
|
129
|
+
<InLoading />
|
|
130
|
+
</ButtonRow>
|
|
131
|
+
</ProcessingRow>
|
|
132
|
+
) : (
|
|
133
|
+
<ButtonRow>
|
|
134
|
+
<ContinueButton
|
|
135
|
+
disabled={!otp}
|
|
136
|
+
onPress={handleCreatePin}
|
|
137
|
+
activeOpacity={0.85}
|
|
138
|
+
>
|
|
139
|
+
<ContinueButtonText>Create Pin</ContinueButtonText>
|
|
140
|
+
</ContinueButton>
|
|
141
|
+
</ButtonRow>
|
|
142
|
+
)}
|
|
143
|
+
</View>
|
|
144
|
+
</Animated.View>
|
|
145
|
+
</View>
|
|
146
|
+
</Modal>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const st = StyleSheet.create({
|
|
151
|
+
scrim: {
|
|
152
|
+
flex: 1,
|
|
153
|
+
backgroundColor: 'rgba(11,29,53,0.6)',
|
|
154
|
+
justifyContent: 'flex-end',
|
|
155
|
+
padding: S.lg,
|
|
156
|
+
paddingBottom: 48,
|
|
157
|
+
},
|
|
158
|
+
card: {
|
|
159
|
+
backgroundColor: C.white,
|
|
160
|
+
borderRadius: 28,
|
|
161
|
+
padding: S.xl,
|
|
162
|
+
alignItems: 'center',
|
|
163
|
+
...shadow.lg,
|
|
164
|
+
},
|
|
165
|
+
avatar: {
|
|
166
|
+
width: 72,
|
|
167
|
+
height: 72,
|
|
168
|
+
borderRadius: 36,
|
|
169
|
+
backgroundColor: C.brandLight,
|
|
170
|
+
justifyContent: 'center',
|
|
171
|
+
alignItems: 'center',
|
|
172
|
+
marginBottom: S.md,
|
|
173
|
+
},
|
|
174
|
+
avatarText: { fontSize: 28, fontWeight: '800', color: C.brand },
|
|
175
|
+
headline: {
|
|
176
|
+
fontSize: F.sm,
|
|
177
|
+
fontWeight: '700',
|
|
178
|
+
color: C.muted,
|
|
179
|
+
letterSpacing: 0.8,
|
|
180
|
+
textTransform: 'uppercase',
|
|
181
|
+
marginBottom: 4,
|
|
182
|
+
},
|
|
183
|
+
sender: { fontSize: F.xl, fontWeight: '800', color: C.ink },
|
|
184
|
+
sub: { fontSize: F.md, color: C.muted, marginBottom: S.sm },
|
|
185
|
+
amount: { fontSize: 40, fontWeight: '800', color: C.ink, marginBottom: S.md },
|
|
186
|
+
noteBox: {
|
|
187
|
+
backgroundColor: C.surface,
|
|
188
|
+
borderRadius: R.md,
|
|
189
|
+
padding: S.md,
|
|
190
|
+
width: '100%',
|
|
191
|
+
marginBottom: S.md,
|
|
192
|
+
},
|
|
193
|
+
noteLabel: { fontSize: F.xs, color: C.muted, marginBottom: 2 },
|
|
194
|
+
note: { fontSize: F.md, color: C.ink },
|
|
195
|
+
disclaimer: {
|
|
196
|
+
fontSize: F.sm,
|
|
197
|
+
color: C.muted,
|
|
198
|
+
textAlign: 'center',
|
|
199
|
+
lineHeight: 20,
|
|
200
|
+
marginBottom: S.lg,
|
|
201
|
+
},
|
|
202
|
+
btnRow: { flexDirection: 'row', gap: S.sm, width: '100%' },
|
|
203
|
+
declineBtn: { flex: 1 },
|
|
204
|
+
acceptBtn: { flex: 1 },
|
|
205
|
+
});
|
|
206
|
+
export default FPCreatePin;
|
|
@@ -16,10 +16,13 @@ import {
|
|
|
16
16
|
import { _onEvent } from '../../engine/FPEngine';
|
|
17
17
|
import SendScreen from '../screens/SendScreen';
|
|
18
18
|
import ReceiveScreen from '../screens/ReceiveScreen';
|
|
19
|
+
import FPValidateBvn from '../modals/FPValidateBvn';
|
|
20
|
+
import FPCreatePin from '../modals/FPCreatePin';
|
|
19
21
|
import { FPPaymentRequestModal } from './FPPaymentRequestModal';
|
|
20
22
|
import { FPEngine } from '../../engine/FPEngine';
|
|
21
23
|
import { C, S, F } from '../theme';
|
|
22
24
|
import type { FPCurrency, FPUserInfo } from '../../core/types';
|
|
25
|
+
import { useFPStore } from '../../store/FPStore';
|
|
23
26
|
|
|
24
27
|
const { width: SCREEN_W, height: SCREEN_H } = Dimensions.get('window');
|
|
25
28
|
|
|
@@ -37,6 +40,8 @@ export function FPShell() {
|
|
|
37
40
|
const [visible, setVisible] = useState(false);
|
|
38
41
|
const slide = useRef(new Animated.Value(SCREEN_H)).current;
|
|
39
42
|
|
|
43
|
+
const user = useFPStore(s => s.user);
|
|
44
|
+
|
|
40
45
|
const open = (state: SheetState) => {
|
|
41
46
|
setSheet(state);
|
|
42
47
|
setVisible(true);
|
|
@@ -89,6 +94,15 @@ export function FPShell() {
|
|
|
89
94
|
return (
|
|
90
95
|
<>
|
|
91
96
|
{/* Always-on BT payment request modal */}
|
|
97
|
+
{user && user?.verifyBvn === false && (
|
|
98
|
+
<FPValidateBvn />
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{/* PIN creation — shown after BVN verified but no PIN yet */}
|
|
102
|
+
{user && user.verifyBvn === true && user.hasPin === false && (
|
|
103
|
+
<FPCreatePin />
|
|
104
|
+
)}
|
|
105
|
+
|
|
92
106
|
<FPPaymentRequestModal />
|
|
93
107
|
|
|
94
108
|
{visible && (
|
|
@@ -98,7 +112,7 @@ export function FPShell() {
|
|
|
98
112
|
<View style={st.content}>
|
|
99
113
|
{sheet.mode === 'send' && (
|
|
100
114
|
<SendScreen
|
|
101
|
-
user={
|
|
115
|
+
user={user ?? null}
|
|
102
116
|
account={FPEngine.getAccount() ?? null}
|
|
103
117
|
amount={sheet.amount ?? 0}
|
|
104
118
|
currency={sheet.currency ?? 'NGN'}
|
|
@@ -113,7 +127,7 @@ export function FPShell() {
|
|
|
113
127
|
<ReceiveScreen
|
|
114
128
|
amount={sheet.amount}
|
|
115
129
|
currency={sheet.currency}
|
|
116
|
-
user={
|
|
130
|
+
user={user ?? null}
|
|
117
131
|
account={FPEngine.getAccount() ?? null}
|
|
118
132
|
onClose={close}
|
|
119
133
|
onPaymentReceived={FPEngine.getCallbacks().onPaymentReceived}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Animated, Modal, StyleSheet, Text, TouchableOpacity, View } from "react-native"
|
|
4
|
+
|
|
5
|
+
import { C, R, S, F, shadow } from '../theme';
|
|
6
|
+
import OTPInputs from "../components/OtpInput";
|
|
7
|
+
import styled from "styled-components/native";
|
|
8
|
+
import InLoading from "../components/LoadingAnimation/InLoading";
|
|
9
|
+
import { FPEngine } from "../../engine/FPEngine";
|
|
10
|
+
import { getFPStore } from "../../store/FPStore";
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const ProcessingRow = styled.View`
|
|
15
|
+
width: 100%;
|
|
16
|
+
display: flex;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
align-items: center;
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const ButtonRow = styled(View)`
|
|
22
|
+
flex-direction: row;
|
|
23
|
+
gap: 12px;
|
|
24
|
+
margin-top: 32px;
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
const ContinueButton = styled(TouchableOpacity)`
|
|
28
|
+
flex: 1;
|
|
29
|
+
background-color: #003333;
|
|
30
|
+
border-radius: 50px;
|
|
31
|
+
padding-vertical: 17px;
|
|
32
|
+
align-items: center;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
shadow-color: #fff;
|
|
35
|
+
shadow-offset: 0px 4px;
|
|
36
|
+
shadow-opacity: 0.3;
|
|
37
|
+
shadow-radius: 10px;
|
|
38
|
+
elevation: 6;
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
const ContinueButtonText = styled(Text)`
|
|
42
|
+
color: #ffffff;
|
|
43
|
+
font-size: 16px;
|
|
44
|
+
font-weight: 700;
|
|
45
|
+
letter-spacing: -0.2px;
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
const SubText = styled(Text)`
|
|
49
|
+
width: 90%;
|
|
50
|
+
font-size: 11px;
|
|
51
|
+
color: #666;
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const Container = styled(View)`
|
|
55
|
+
width: 100%;
|
|
56
|
+
display: flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
gap: 10px;
|
|
60
|
+
`
|
|
61
|
+
|
|
62
|
+
const FPValidateBvn = () => {
|
|
63
|
+
const [processing, setProcessing] = useState(false);
|
|
64
|
+
const scale = React.useRef(new Animated.Value(0.85)).current;
|
|
65
|
+
const buttonAnim = React.useRef(new Animated.Value(0)).current;
|
|
66
|
+
const [otp, setOtp] = useState<string>('');
|
|
67
|
+
const [error, setError] = useState("")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
const resendSms = async()=>{
|
|
71
|
+
setError("");
|
|
72
|
+
setProcessing(true);
|
|
73
|
+
try{
|
|
74
|
+
// TODO: Implement BVN validation logic here
|
|
75
|
+
await FPEngine.verifyBvn();
|
|
76
|
+
} catch (error: any) {
|
|
77
|
+
console.error('Error validating BVN:', error);
|
|
78
|
+
setError(error.message || "BVN not found")
|
|
79
|
+
} finally {
|
|
80
|
+
setProcessing(false);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const handleValidateBvn = async() => {
|
|
85
|
+
setProcessing(true);
|
|
86
|
+
setError("");
|
|
87
|
+
try{
|
|
88
|
+
// TODO: Implement BVN validation logic here
|
|
89
|
+
const values: any = {
|
|
90
|
+
otp,
|
|
91
|
+
temp_id: getFPStore().tempId
|
|
92
|
+
};
|
|
93
|
+
await FPEngine.verifySmsOtp(values);
|
|
94
|
+
} catch (error: any) {
|
|
95
|
+
console.error('Error validating BVN:', error);
|
|
96
|
+
setError(error.message || "Invalid token")
|
|
97
|
+
} finally {
|
|
98
|
+
setProcessing(false);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
React.useEffect(() => {
|
|
103
|
+
resendSms();
|
|
104
|
+
Animated.sequence([
|
|
105
|
+
Animated.timing(buttonAnim, {
|
|
106
|
+
toValue: 1,
|
|
107
|
+
duration: 300,
|
|
108
|
+
useNativeDriver: true,
|
|
109
|
+
}),
|
|
110
|
+
]).start();
|
|
111
|
+
}, []);
|
|
112
|
+
return (
|
|
113
|
+
<Modal
|
|
114
|
+
visible
|
|
115
|
+
transparent
|
|
116
|
+
animationType="slide"
|
|
117
|
+
statusBarTranslucent
|
|
118
|
+
>
|
|
119
|
+
<View
|
|
120
|
+
style={{
|
|
121
|
+
flex: 1,
|
|
122
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
123
|
+
justifyContent: 'center',
|
|
124
|
+
alignItems: 'center',
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
<Animated.View style={{ transform: [{ scale }] }}>
|
|
128
|
+
<View
|
|
129
|
+
style={[st.card, {
|
|
130
|
+
borderRadius: 20,
|
|
131
|
+
padding: 30,
|
|
132
|
+
}]}
|
|
133
|
+
>
|
|
134
|
+
|
|
135
|
+
<Container>
|
|
136
|
+
<Text style={{ color: C.brand, fontSize: 16, fontWeight: '600', marginBottom: 6 }}>
|
|
137
|
+
Validate your BVN
|
|
138
|
+
</Text>
|
|
139
|
+
<OTPInputs count={4} callBack={setOtp} />
|
|
140
|
+
<SubText>SMS has been sent to the phone number tied to your BVN</SubText>
|
|
141
|
+
<TouchableOpacity onPress={resendSms}>
|
|
142
|
+
<Text>Resend token</Text>
|
|
143
|
+
</TouchableOpacity>
|
|
144
|
+
{error && <Text style={{ color: 'red', fontSize: 12, marginTop: 6 }}>{error}</Text>}
|
|
145
|
+
</Container>
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
{processing ? (
|
|
149
|
+
<ProcessingRow>
|
|
150
|
+
<ButtonRow>
|
|
151
|
+
<InLoading />
|
|
152
|
+
</ButtonRow>
|
|
153
|
+
</ProcessingRow>
|
|
154
|
+
) : (
|
|
155
|
+
<ButtonRow>
|
|
156
|
+
<ContinueButton
|
|
157
|
+
disabled={!otp}
|
|
158
|
+
onPress={handleValidateBvn}
|
|
159
|
+
activeOpacity={0.85}
|
|
160
|
+
>
|
|
161
|
+
<ContinueButtonText>Verify Me</ContinueButtonText>
|
|
162
|
+
</ContinueButton>
|
|
163
|
+
</ButtonRow>
|
|
164
|
+
)}
|
|
165
|
+
</View>
|
|
166
|
+
</Animated.View>
|
|
167
|
+
</View>
|
|
168
|
+
</Modal>
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const st = StyleSheet.create({
|
|
173
|
+
scrim: {
|
|
174
|
+
flex: 1,
|
|
175
|
+
backgroundColor: 'rgba(11,29,53,0.6)',
|
|
176
|
+
justifyContent: 'flex-end',
|
|
177
|
+
padding: S.lg,
|
|
178
|
+
paddingBottom: 48,
|
|
179
|
+
},
|
|
180
|
+
card: {
|
|
181
|
+
width: '70%',
|
|
182
|
+
backgroundColor: C.white,
|
|
183
|
+
borderRadius: 28,
|
|
184
|
+
padding: S.xl,
|
|
185
|
+
alignItems: 'center',
|
|
186
|
+
...shadow.lg,
|
|
187
|
+
},
|
|
188
|
+
avatar: {
|
|
189
|
+
width: 72,
|
|
190
|
+
height: 72,
|
|
191
|
+
borderRadius: 36,
|
|
192
|
+
backgroundColor: C.brandLight,
|
|
193
|
+
justifyContent: 'center',
|
|
194
|
+
alignItems: 'center',
|
|
195
|
+
marginBottom: S.md,
|
|
196
|
+
},
|
|
197
|
+
avatarText: { fontSize: 28, fontWeight: '800', color: C.brand },
|
|
198
|
+
headline: {
|
|
199
|
+
fontSize: F.sm,
|
|
200
|
+
fontWeight: '700',
|
|
201
|
+
color: C.muted,
|
|
202
|
+
letterSpacing: 0.8,
|
|
203
|
+
textTransform: 'uppercase',
|
|
204
|
+
marginBottom: 4,
|
|
205
|
+
},
|
|
206
|
+
sender: { fontSize: F.xl, fontWeight: '800', color: C.ink },
|
|
207
|
+
sub: { fontSize: F.md, color: C.muted, marginBottom: S.sm },
|
|
208
|
+
amount: { fontSize: 40, fontWeight: '800', color: C.ink, marginBottom: S.md },
|
|
209
|
+
noteBox: {
|
|
210
|
+
backgroundColor: C.surface,
|
|
211
|
+
borderRadius: R.md,
|
|
212
|
+
padding: S.md,
|
|
213
|
+
width: '100%',
|
|
214
|
+
marginBottom: S.md,
|
|
215
|
+
},
|
|
216
|
+
noteLabel: { fontSize: F.xs, color: C.muted, marginBottom: 2 },
|
|
217
|
+
note: { fontSize: F.md, color: C.ink },
|
|
218
|
+
disclaimer: {
|
|
219
|
+
fontSize: F.sm,
|
|
220
|
+
color: C.muted,
|
|
221
|
+
textAlign: 'center',
|
|
222
|
+
lineHeight: 20,
|
|
223
|
+
marginBottom: S.lg,
|
|
224
|
+
},
|
|
225
|
+
btnRow: { flexDirection: 'row', gap: S.sm, width: '100%' },
|
|
226
|
+
declineBtn: { flex: 1 },
|
|
227
|
+
acceptBtn: { flex: 1 },
|
|
228
|
+
});
|
|
229
|
+
export default FPValidateBvn;
|
|
@@ -206,9 +206,9 @@ interface Props {
|
|
|
206
206
|
export function ResultScreen({ transaction, onClose }: Props) {
|
|
207
207
|
const [loading, setLoading] = useState<boolean>(false);
|
|
208
208
|
const [transactionDetail, setTransactionDetail] = useState<any>(null)
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
const scaleAnim = useRef(new Animated.Value(
|
|
209
|
+
const opacAnim = useRef(new Animated.Value(1)).current; // ← start visible
|
|
210
|
+
const slideAnim = useRef(new Animated.Value(0)).current; // ← start in place
|
|
211
|
+
const scaleAnim = useRef(new Animated.Value(1)).current; // ← start full size
|
|
212
212
|
|
|
213
213
|
const isSuccess = transactionDetail?.status.toLowerCase() === 'successful';
|
|
214
214
|
const accentColor = isSuccess ? C.green : C.red;
|
|
@@ -250,19 +250,14 @@ export function ResultScreen({ transaction, onClose }: Props) {
|
|
|
250
250
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
|
|
254
|
-
Animated.parallel([
|
|
255
|
-
Animated.spring(scaleAnim, { toValue: 1, useNativeDriver: true }),
|
|
256
|
-
Animated.timing(opacAnim, { toValue: 1, duration: 300, useNativeDriver: true }),
|
|
257
|
-
Animated.timing(slideAnim, { toValue: 0, duration: 350, useNativeDriver: true }),
|
|
258
|
-
]).start();
|
|
259
|
-
}, [scaleAnim, opacAnim, slideAnim]);
|
|
253
|
+
|
|
260
254
|
|
|
261
255
|
useEffect(() => {
|
|
262
256
|
const timer = setTimeout(onClose, COUNTDOWN_SECONDS * 1000);
|
|
263
257
|
return () => clearTimeout(timer);
|
|
264
258
|
}, [onClose]);
|
|
265
259
|
|
|
260
|
+
|
|
266
261
|
useEffect(()=>{
|
|
267
262
|
if(transaction){
|
|
268
263
|
loadTransaction();
|
|
@@ -274,6 +269,7 @@ export function ResultScreen({ transaction, onClose }: Props) {
|
|
|
274
269
|
return <Gradients />;
|
|
275
270
|
}
|
|
276
271
|
|
|
272
|
+
|
|
277
273
|
return (
|
|
278
274
|
<Container style={{ opacity: opacAnim, transform: [{ translateY: slideAnim }] }}>
|
|
279
275
|
|