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,158 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { View, Text, Modal, StyleSheet, Animated, TouchableOpacity } from 'react-native';
|
|
3
|
+
import { FPEngine, _onEvent } from '../../engine/FPEngine';
|
|
4
|
+
import { FPButton } from '../components/FPButton';
|
|
5
|
+
import { C, R, S, F, shadow } from '../theme';
|
|
6
|
+
import type { FPBluetoothPaymentRequest } from '../../core/types';
|
|
7
|
+
|
|
8
|
+
export function FPPaymentRequestModal() {
|
|
9
|
+
const [request, setRequest] = useState<FPBluetoothPaymentRequest | null>(null);
|
|
10
|
+
const [loading, setLoading] = useState(false);
|
|
11
|
+
const scale = React.useRef(new Animated.Value(0.85)).current;
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const unsub = _onEvent('incoming_payment_request', (req: unknown) => {
|
|
15
|
+
setRequest(req as FPBluetoothPaymentRequest);
|
|
16
|
+
Animated.spring(scale, { toValue: 1, useNativeDriver: true, tension: 80, friction: 10 }).start();
|
|
17
|
+
});
|
|
18
|
+
return unsub;
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
const dismiss = () => { scale.setValue(0.85); setRequest(null); setLoading(false); };
|
|
22
|
+
|
|
23
|
+
const handleAccept = async () => {
|
|
24
|
+
if (!request) return;
|
|
25
|
+
setLoading(true);
|
|
26
|
+
await FPEngine.acceptPaymentRequest(request);
|
|
27
|
+
setLoading(false);
|
|
28
|
+
dismiss();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleDecline = () => {
|
|
32
|
+
if (!request) return;
|
|
33
|
+
FPEngine.declinePaymentRequest(request);
|
|
34
|
+
dismiss();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (!request) return null;
|
|
38
|
+
|
|
39
|
+
const user = FPEngine.getUser();
|
|
40
|
+
const amountFormatted = (request.amount / 100).toLocaleString('en-NG', { minimumFractionDigits: 2 });
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Modal
|
|
44
|
+
visible transparent animationType="slide" statusBarTranslucent onRequestClose={handleDecline}
|
|
45
|
+
>
|
|
46
|
+
<View
|
|
47
|
+
style={{
|
|
48
|
+
flex: 1,
|
|
49
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
50
|
+
justifyContent: 'center',
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
|
|
55
|
+
<View
|
|
56
|
+
style={{
|
|
57
|
+
backgroundColor: '#fff',
|
|
58
|
+
borderRadius: 20,
|
|
59
|
+
padding: 30,
|
|
60
|
+
width: '80%',
|
|
61
|
+
maxWidth: 400,
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<Animated.View style={[st.card, { transform: [{ scale }] }]}>
|
|
65
|
+
<View style={st.avatar}>
|
|
66
|
+
<Text style={st.avatarText}>
|
|
67
|
+
{(request?.sender.accountName ?? request?.sender.deviceName ?? '?').charAt(0).toUpperCase()}
|
|
68
|
+
</Text>
|
|
69
|
+
</View>
|
|
70
|
+
<Text style={st.headline}>Payment Request</Text>
|
|
71
|
+
|
|
72
|
+
<View style={{ marginBottom: 30 }}>
|
|
73
|
+
<Text
|
|
74
|
+
style={{
|
|
75
|
+
fontSize: 18,
|
|
76
|
+
color: '#666',
|
|
77
|
+
textAlign: 'center',
|
|
78
|
+
marginBottom: 10,
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
From
|
|
82
|
+
</Text>
|
|
83
|
+
<Text
|
|
84
|
+
style={{
|
|
85
|
+
fontSize: 20,
|
|
86
|
+
fontWeight: 'bold',
|
|
87
|
+
textAlign: 'center',
|
|
88
|
+
marginBottom: 20,
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
{request.sender.accountName ?? request.sender.deviceName ?? 'Someone'}
|
|
92
|
+
</Text>
|
|
93
|
+
<Text style={st.sub}>wants to send you</Text>
|
|
94
|
+
{request.amount &&(
|
|
95
|
+
<View
|
|
96
|
+
style={{
|
|
97
|
+
backgroundColor: '#f5f5f5',
|
|
98
|
+
padding: 20,
|
|
99
|
+
borderRadius: 10,
|
|
100
|
+
alignItems: 'center',
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<Text style={{ fontSize: 16, color: '#666', marginBottom: 5 }}>
|
|
104
|
+
Amount
|
|
105
|
+
</Text>
|
|
106
|
+
<Text
|
|
107
|
+
style={{
|
|
108
|
+
fontSize: 36,
|
|
109
|
+
fontWeight: 'bold',
|
|
110
|
+
color: '#4CAF50',
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{request.currency}{request.amount.toFixed(2)}
|
|
114
|
+
</Text>
|
|
115
|
+
</View>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{request.narration ? (
|
|
119
|
+
<View style={st.noteBox}>
|
|
120
|
+
<Text style={st.noteLabel}>Note</Text>
|
|
121
|
+
<Text style={st.note}>{request.narration}</Text>
|
|
122
|
+
</View>
|
|
123
|
+
) : null}
|
|
124
|
+
<Text style={st.disclaimer}>
|
|
125
|
+
Accepting will receive into{'\n'}
|
|
126
|
+
<Text style={{ fontWeight: '700' }}>{user?.accountName}</Text> ({user?.accountNumber})
|
|
127
|
+
</Text>
|
|
128
|
+
</View>
|
|
129
|
+
|
|
130
|
+
<View style={st.btnRow}>
|
|
131
|
+
<FPButton label="Decline" onPress={handleDecline} variant="outline" style={st.declineBtn} />
|
|
132
|
+
<FPButton label="Accept" onPress={handleAccept} loading={loading} style={st.acceptBtn} />
|
|
133
|
+
</View>
|
|
134
|
+
</Animated.View>
|
|
135
|
+
|
|
136
|
+
</View>
|
|
137
|
+
</View>
|
|
138
|
+
</Modal>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const st = StyleSheet.create({
|
|
143
|
+
scrim: { flex: 1, backgroundColor: 'rgba(11,29,53,0.6)', justifyContent: 'flex-end', padding: S.lg, paddingBottom: 48 },
|
|
144
|
+
card: { backgroundColor: C.white, borderRadius: 28, padding: S.xl, alignItems: 'center', ...shadow.lg },
|
|
145
|
+
avatar: { width: 72, height: 72, borderRadius: 36, backgroundColor: C.brandLight, justifyContent: 'center', alignItems: 'center', marginBottom: S.md },
|
|
146
|
+
avatarText: { fontSize: 28, fontWeight: '800', color: C.brand },
|
|
147
|
+
headline: { fontSize: F.sm, fontWeight: '700', color: C.muted, letterSpacing: 0.8, textTransform: 'uppercase', marginBottom: 4 },
|
|
148
|
+
sender: { fontSize: F.xl, fontWeight: '800', color: C.ink },
|
|
149
|
+
sub: { fontSize: F.md, color: C.muted, marginBottom: S.sm },
|
|
150
|
+
amount: { fontSize: 40, fontWeight: '800', color: C.ink, marginBottom: S.md },
|
|
151
|
+
noteBox: { backgroundColor: C.surface, borderRadius: R.md, padding: S.md, width: '100%', marginBottom: S.md },
|
|
152
|
+
noteLabel: { fontSize: F.xs, color: C.muted, marginBottom: 2 },
|
|
153
|
+
note: { fontSize: F.md, color: C.ink },
|
|
154
|
+
disclaimer: { fontSize: F.sm, color: C.muted, textAlign: 'center', lineHeight: 20, marginBottom: S.lg },
|
|
155
|
+
btnRow: { flexDirection: 'row', gap: S.sm, width: '100%' },
|
|
156
|
+
declineBtn: { flex: 1 },
|
|
157
|
+
acceptBtn: { flex: 1 },
|
|
158
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────
|
|
2
|
+
// FPShell — The master modal container
|
|
3
|
+
// Mounts invisibly at app root. Listens for
|
|
4
|
+
// show_send / show_receive engine events and
|
|
5
|
+
// renders the appropriate full-screen sheet.
|
|
6
|
+
// ─────────────────────────────────────────────
|
|
7
|
+
import React, { useEffect, useState, useRef } from 'react';
|
|
8
|
+
import { Modal, View, Text, TouchableOpacity, StyleSheet, Animated, Dimensions, SafeAreaView } from 'react-native';
|
|
9
|
+
import { _onEvent } from '../../engine/FPEngine';
|
|
10
|
+
import { SendScreen } from '../screens/SendScreen';
|
|
11
|
+
import { ReceiveScreen } from '../screens/ReceiveScreen';
|
|
12
|
+
import { FPPaymentRequestModal } from './FPPaymentRequestModal';
|
|
13
|
+
import { FPEngine } from '../../engine/FPEngine';
|
|
14
|
+
import { C, S, F, shadow } from '../theme';
|
|
15
|
+
import type { FPCurrency } from '../../core/types';
|
|
16
|
+
|
|
17
|
+
const { height: SCREEN_H } = Dimensions.get('window');
|
|
18
|
+
|
|
19
|
+
type SheetMode = 'send' | 'receive' | null;
|
|
20
|
+
|
|
21
|
+
interface SheetState {
|
|
22
|
+
mode: SheetMode;
|
|
23
|
+
amount?: number;
|
|
24
|
+
currency?: FPCurrency;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function FPShell() {
|
|
28
|
+
const [sheet, setSheet] = useState<SheetState>({ mode: null });
|
|
29
|
+
const slide = useRef(new Animated.Value(SCREEN_H)).current;
|
|
30
|
+
|
|
31
|
+
const open = (state: SheetState) => {
|
|
32
|
+
setSheet(state);
|
|
33
|
+
Animated.spring(slide, { toValue: 0, useNativeDriver: true, tension: 65, friction: 11 }).start();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const close = () => {
|
|
37
|
+
Animated.timing(slide, { toValue: SCREEN_H, duration: 260, useNativeDriver: true }).start(() => {
|
|
38
|
+
setSheet({ mode: null });
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const unsubSend = _onEvent('show_send', (d: unknown) => { const data = d as any; open({ mode: 'send', amount: data?.amount, currency: data?.currency }); });
|
|
44
|
+
const unsubReceive = _onEvent('show_receive', (d: unknown) => { const data = d as any; open({ mode: 'receive', amount: data?.amount, currency: data?.currency }); });
|
|
45
|
+
return () => { unsubSend(); unsubReceive(); };
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
const user = FPEngine.getUser();
|
|
49
|
+
const callbacks = FPEngine.getCallbacks();
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<>
|
|
53
|
+
{/* Always-on BT payment request modal */}
|
|
54
|
+
<FPPaymentRequestModal />
|
|
55
|
+
|
|
56
|
+
{/* Send / Receive sheet */}
|
|
57
|
+
<Modal visible={sheet.mode !== null} transparent animationType="none" statusBarTranslucent onRequestClose={close}>
|
|
58
|
+
<TouchableOpacity style={st.scrim} activeOpacity={1} onPress={close} />
|
|
59
|
+
<Animated.View style={[st.sheet, { transform: [{ translateY: slide }] }]}>
|
|
60
|
+
<SafeAreaView style={{ flex: 1 }}>
|
|
61
|
+
{/* Handle + header */}
|
|
62
|
+
<View style={st.handle} />
|
|
63
|
+
<View style={st.header}>
|
|
64
|
+
<Text style={st.headerTitle}>
|
|
65
|
+
{sheet.mode === 'send' ? 'Send Money' : 'Receive Money'}
|
|
66
|
+
</Text>
|
|
67
|
+
<TouchableOpacity onPress={close} style={st.closeBtn}>
|
|
68
|
+
<Text style={st.closeText}>Done</Text>
|
|
69
|
+
</TouchableOpacity>
|
|
70
|
+
</View>
|
|
71
|
+
|
|
72
|
+
{/* Content */}
|
|
73
|
+
{sheet.mode === 'send' && user && (
|
|
74
|
+
<SendScreen
|
|
75
|
+
amount={sheet.amount ?? 0}
|
|
76
|
+
currency={sheet.currency ?? 'NGN'}
|
|
77
|
+
onClose={close}
|
|
78
|
+
onPaymentSent={(tx) => { callbacks.onPaymentSent?.(tx); close(); }}
|
|
79
|
+
onError={callbacks.onError}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
{sheet.mode === 'receive' && user && (
|
|
83
|
+
<ReceiveScreen
|
|
84
|
+
amount={sheet.amount}
|
|
85
|
+
currency={sheet.currency}
|
|
86
|
+
user={user}
|
|
87
|
+
onClose={close}
|
|
88
|
+
onPaymentReceived={callbacks.onPaymentReceived}
|
|
89
|
+
onError={callbacks.onError}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
</SafeAreaView>
|
|
93
|
+
</Animated.View>
|
|
94
|
+
</Modal>
|
|
95
|
+
</>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const st = StyleSheet.create({
|
|
100
|
+
scrim: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(11,29,53,0.55)' },
|
|
101
|
+
sheet: { position: 'absolute', bottom: 0, left: 0, right: 0, height: SCREEN_H * 0.92, backgroundColor: C.white, borderTopLeftRadius: 24, borderTopRightRadius: 24, ...shadow.lg },
|
|
102
|
+
handle: { width: 40, height: 4, borderRadius: 2, backgroundColor: '#DFE1E6', alignSelf: 'center', marginTop: S.sm },
|
|
103
|
+
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: S.lg, paddingVertical: S.md, borderBottomWidth: 1, borderBottomColor: '#DFE1E6' },
|
|
104
|
+
headerTitle: { fontSize: F.lg, fontWeight: '800', color: C.ink },
|
|
105
|
+
closeBtn: { paddingHorizontal: S.sm, paddingVertical: 4 },
|
|
106
|
+
closeText: { fontSize: F.md, color: '#0052CC', fontWeight: '700' },
|
|
107
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, Text, TouchableOpacity, StyleSheet, ScrollView } from 'react-native';
|
|
3
|
+
import { C, R, S, F, shadow } from '../theme';
|
|
4
|
+
import { NQRSubScreen } from './sub/NQRSubScreen';
|
|
5
|
+
import { NFCSubScreen } from './sub/NFCSubScreen';
|
|
6
|
+
import type { FPCurrency, FPCallbacks, FPUserInfo } from '../../core/types';
|
|
7
|
+
|
|
8
|
+
type Channel = 'transfer' | 'nqr' | 'nfc';
|
|
9
|
+
|
|
10
|
+
const CHANNELS: { id: Channel; emoji: string; title: string; desc: string }[] = [
|
|
11
|
+
{ id: 'transfer', emoji: '🏦', title: 'Bank Transfer', desc: 'Share your account number' },
|
|
12
|
+
{ id: 'nqr', emoji: '⬛', title: 'NQR Code', desc: 'Generate a QR for others to scan' },
|
|
13
|
+
{ id: 'nfc', emoji: '📲', title: 'NFC Tap', desc: 'Let someone tap to pay you' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
interface Props extends FPCallbacks {
|
|
17
|
+
amount?: number;
|
|
18
|
+
currency?: FPCurrency;
|
|
19
|
+
user: FPUserInfo;
|
|
20
|
+
onClose: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ReceiveScreen({ amount, currency = 'NGN', user, onClose, onPaymentReceived, onError }: Props) {
|
|
24
|
+
const [channel, setChannel] = useState<Channel | null>(null);
|
|
25
|
+
|
|
26
|
+
const myWallet = {
|
|
27
|
+
accountName: user.accountName,
|
|
28
|
+
accountNumber: user.accountNumber,
|
|
29
|
+
bankName: user.bankName,
|
|
30
|
+
bankCode: user.bankCode,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (channel === 'nqr') return <NQRSubScreen mode="receive" amount={amount} currency={currency} onBack={() => setChannel(null)} onDone={onClose} onError={onError} />;
|
|
34
|
+
if (channel === 'nfc') return <NFCSubScreen mode="receive" myWallet={myWallet} onBack={() => setChannel(null)} onDone={onClose} onError={onError} />;
|
|
35
|
+
|
|
36
|
+
// Transfer — just show account details inline
|
|
37
|
+
if (channel === 'transfer') return (
|
|
38
|
+
<View style={styles.container}>
|
|
39
|
+
<TouchableOpacity onPress={() => setChannel(null)} style={styles.back}>
|
|
40
|
+
<Text style={styles.backText}>‹ Back</Text>
|
|
41
|
+
</TouchableOpacity>
|
|
42
|
+
<Text style={styles.sectionTitle}>Your Account Details</Text>
|
|
43
|
+
<View style={styles.accountCard}>
|
|
44
|
+
<Row label="Account Name" value={user.accountName} />
|
|
45
|
+
<Row label="Account Number" value={user.accountNumber} large copyable />
|
|
46
|
+
<Row label="Bank" value={user.bankName} />
|
|
47
|
+
{amount && <Row label="Expected Amount" value={`${currency} ${amount.toLocaleString('en-NG', { minimumFractionDigits: 2 })}`} highlight />}
|
|
48
|
+
</View>
|
|
49
|
+
<Text style={styles.hint}>Share these details with whoever is paying you</Text>
|
|
50
|
+
</View>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<View style={styles.container}>
|
|
55
|
+
{amount && (
|
|
56
|
+
<View style={styles.amountBanner}>
|
|
57
|
+
<Text style={styles.amountLabel}>Receiving</Text>
|
|
58
|
+
<Text style={styles.amountValue}>{currency} {amount.toLocaleString('en-NG', { minimumFractionDigits: 2 })}</Text>
|
|
59
|
+
</View>
|
|
60
|
+
)}
|
|
61
|
+
<Text style={styles.sectionTitle}>How do you want to receive?</Text>
|
|
62
|
+
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={styles.list}>
|
|
63
|
+
{CHANNELS.map(ch => (
|
|
64
|
+
<TouchableOpacity key={ch.id} style={styles.card} onPress={() => setChannel(ch.id)} activeOpacity={0.8}>
|
|
65
|
+
<View style={styles.cardIcon}><Text style={styles.cardEmoji}>{ch.emoji}</Text></View>
|
|
66
|
+
<View style={styles.cardText}>
|
|
67
|
+
<Text style={styles.cardTitle}>{ch.title}</Text>
|
|
68
|
+
<Text style={styles.cardDesc}>{ch.desc}</Text>
|
|
69
|
+
</View>
|
|
70
|
+
<Text style={styles.arrow}>›</Text>
|
|
71
|
+
</TouchableOpacity>
|
|
72
|
+
))}
|
|
73
|
+
</ScrollView>
|
|
74
|
+
</View>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function Row({ label, value, large, copyable, highlight }: { label: string; value: string; large?: boolean; copyable?: boolean; highlight?: boolean }) {
|
|
79
|
+
const [copied, setCopied] = React.useState(false);
|
|
80
|
+
return (
|
|
81
|
+
<View style={rowStyles.row}>
|
|
82
|
+
<Text style={rowStyles.label}>{label}</Text>
|
|
83
|
+
<TouchableOpacity onPress={copyable ? () => { require('react-native').Clipboard.setString(value); setCopied(true); setTimeout(() => setCopied(false), 2000); } : undefined}>
|
|
84
|
+
<Text style={[rowStyles.value, large && rowStyles.large, highlight && rowStyles.highlight]}>
|
|
85
|
+
{value}{copyable && <Text style={rowStyles.copy}> {copied ? '✓' : 'Copy'}</Text>}
|
|
86
|
+
</Text>
|
|
87
|
+
</TouchableOpacity>
|
|
88
|
+
</View>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const rowStyles = StyleSheet.create({
|
|
93
|
+
row: { paddingVertical: S.sm, borderBottomWidth: 1, borderBottomColor: C.border },
|
|
94
|
+
label: { fontSize: F.xs, color: C.muted, marginBottom: 2 },
|
|
95
|
+
value: { fontSize: F.md, color: C.ink, fontWeight: '600' },
|
|
96
|
+
large: { fontSize: F.xl, fontWeight: '800', letterSpacing: 2 },
|
|
97
|
+
highlight: { color: C.green, fontWeight: '800' },
|
|
98
|
+
copy: { fontSize: F.sm, color: C.brand, fontWeight: '700' },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const styles = StyleSheet.create({
|
|
102
|
+
container: { flex: 1, padding: S.lg },
|
|
103
|
+
back: { marginBottom: S.md },
|
|
104
|
+
backText: { color: C.brand, fontSize: F.md, fontWeight: '600' },
|
|
105
|
+
amountBanner: { backgroundColor: C.greenLight, borderRadius: R.xl, padding: S.lg, alignItems: 'center', marginBottom: S.lg },
|
|
106
|
+
amountLabel: { color: C.green, fontSize: F.sm, marginBottom: 4 },
|
|
107
|
+
amountValue: { color: C.green, fontSize: F.hero, fontWeight: '800' },
|
|
108
|
+
sectionTitle: { fontSize: F.sm, color: C.muted, fontWeight: '600', letterSpacing: 0.6, textTransform: 'uppercase', marginBottom: S.md },
|
|
109
|
+
list: { paddingBottom: S.xl },
|
|
110
|
+
card: { flexDirection: 'row', alignItems: 'center', backgroundColor: C.surface, borderRadius: R.lg, padding: S.md, marginBottom: S.sm, ...shadow.sm },
|
|
111
|
+
cardIcon: { width: 48, height: 48, borderRadius: R.md, backgroundColor: C.white, justifyContent: 'center', alignItems: 'center', marginRight: S.md },
|
|
112
|
+
cardEmoji: { fontSize: 24 },
|
|
113
|
+
cardText: { flex: 1 },
|
|
114
|
+
cardTitle: { fontSize: F.md, fontWeight: '700', color: C.ink },
|
|
115
|
+
cardDesc: { fontSize: F.sm, color: C.muted, marginTop: 2 },
|
|
116
|
+
arrow: { fontSize: F.xl, color: C.ghost },
|
|
117
|
+
accountCard: { backgroundColor: C.surface, borderRadius: R.lg, padding: S.md, marginBottom: S.md },
|
|
118
|
+
hint: { fontSize: F.sm, color: C.muted, textAlign: 'center' },
|
|
119
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────
|
|
2
|
+
// Send Money Screen
|
|
3
|
+
// Shows channel selection, then opens the
|
|
4
|
+
// relevant sub-screen in the same modal sheet
|
|
5
|
+
// ─────────────────────────────────────────────
|
|
6
|
+
import React, { useState } from 'react';
|
|
7
|
+
import { View, Text, TouchableOpacity, StyleSheet, ScrollView } from 'react-native';
|
|
8
|
+
import { C, R, S, F, shadow } from '../theme';
|
|
9
|
+
import { FPButton } from '../components/FPButton';
|
|
10
|
+
import { TransferSubScreen } from './sub/TransferSubScreen';
|
|
11
|
+
import { NQRSubScreen } from './sub/NQRSubScreen';
|
|
12
|
+
import { ProximitySubScreen } from './sub/ProximitySubScreen';
|
|
13
|
+
import { BluetoothSubScreen } from './sub/BluetoothSubScreen';
|
|
14
|
+
import { NFCSubScreen } from './sub/NFCSubScreen';
|
|
15
|
+
import type { FPCurrency, FPCallbacks } from '../../core/types';
|
|
16
|
+
|
|
17
|
+
type Channel = 'transfer' | 'nqr' | 'proximity' | 'bluetooth' | 'nfc';
|
|
18
|
+
|
|
19
|
+
const CHANNELS: { id: Channel; emoji: string; title: string; desc: string }[] = [
|
|
20
|
+
{ id: 'transfer', emoji: '🏦', title: 'Bank Transfer', desc: 'Send to any Nigerian bank account' },
|
|
21
|
+
{ id: 'nqr', emoji: '⬛', title: 'NQR Scan', desc: 'Scan a merchant or person QR code' },
|
|
22
|
+
{ id: 'proximity', emoji: '📡', title: 'Nearby', desc: 'People within 100m of you' },
|
|
23
|
+
{ id: 'bluetooth', emoji: '📶', title: 'Bluetooth', desc: 'Pair wirelessly and send' },
|
|
24
|
+
{ id: 'nfc', emoji: '📲', title: 'NFC Tap', desc: 'Tap another phone to pay' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
interface Props extends FPCallbacks {
|
|
28
|
+
amount: number;
|
|
29
|
+
currency: FPCurrency;
|
|
30
|
+
onClose: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function SendScreen({ amount, currency, onClose, onPaymentSent, onError }: Props) {
|
|
34
|
+
const [channel, setChannel] = useState<Channel | null>(null);
|
|
35
|
+
|
|
36
|
+
const formatted = `${currency} ${(amount).toLocaleString('en-NG', { minimumFractionDigits: 2 })}`;
|
|
37
|
+
|
|
38
|
+
if (channel === 'transfer') return <TransferSubScreen amount={amount} currency={currency} onBack={() => setChannel(null)} onDone={onClose} onSuccess={onPaymentSent} onError={onError} />;
|
|
39
|
+
if (channel === 'nqr') return <NQRSubScreen mode="send" amount={amount} currency={currency} onBack={() => setChannel(null)} onDone={onClose} onSuccess={onPaymentSent} onError={onError} />;
|
|
40
|
+
if (channel === 'proximity') return <ProximitySubScreen amount={amount} currency={currency} onBack={() => setChannel(null)} onDone={onClose} onSuccess={onPaymentSent} onError={onError} />;
|
|
41
|
+
if (channel === 'bluetooth') return <BluetoothSubScreen mode="send" amount={amount} currency={currency} onBack={() => setChannel(null)} onDone={onClose} onSuccess={onPaymentSent} onError={onError} />;
|
|
42
|
+
if (channel === 'nfc') return <NFCSubScreen mode="send" amount={amount} currency={currency} onBack={() => setChannel(null)} onDone={onClose} onSuccess={onPaymentSent} onError={onError} />;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<View style={styles.container}>
|
|
46
|
+
{/* Amount banner */}
|
|
47
|
+
<View style={styles.amountBanner}>
|
|
48
|
+
<Text style={styles.amountLabel}>Sending</Text>
|
|
49
|
+
<Text style={styles.amountValue}>{formatted}</Text>
|
|
50
|
+
</View>
|
|
51
|
+
|
|
52
|
+
<Text style={styles.sectionTitle}>How do you want to send?</Text>
|
|
53
|
+
|
|
54
|
+
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={styles.channelList}>
|
|
55
|
+
{CHANNELS.map(ch => (
|
|
56
|
+
<TouchableOpacity key={ch.id} style={styles.card} onPress={() => setChannel(ch.id)} activeOpacity={0.8}>
|
|
57
|
+
<View style={styles.cardIcon}>
|
|
58
|
+
<Text style={styles.cardEmoji}>{ch.emoji}</Text>
|
|
59
|
+
</View>
|
|
60
|
+
<View style={styles.cardText}>
|
|
61
|
+
<Text style={styles.cardTitle}>{ch.title}</Text>
|
|
62
|
+
<Text style={styles.cardDesc}>{ch.desc}</Text>
|
|
63
|
+
</View>
|
|
64
|
+
<Text style={styles.arrow}>›</Text>
|
|
65
|
+
</TouchableOpacity>
|
|
66
|
+
))}
|
|
67
|
+
</ScrollView>
|
|
68
|
+
</View>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const styles = StyleSheet.create({
|
|
73
|
+
container: { flex: 1 },
|
|
74
|
+
amountBanner: { backgroundColor: C.brand, marginHorizontal: S.lg, borderRadius: R.xl, padding: S.lg, alignItems: 'center', marginBottom: S.lg },
|
|
75
|
+
amountLabel: { color: 'rgba(255,255,255,0.7)', fontSize: F.sm, marginBottom: 4 },
|
|
76
|
+
amountValue: { color: C.white, fontSize: F.hero, fontWeight: '800' },
|
|
77
|
+
sectionTitle: { fontSize: F.sm, color: C.muted, fontWeight: '600', letterSpacing: 0.6, textTransform: 'uppercase', paddingHorizontal: S.lg, marginBottom: S.md },
|
|
78
|
+
channelList: { paddingHorizontal: S.lg, paddingBottom: S.xl },
|
|
79
|
+
card: { flexDirection: 'row', alignItems: 'center', backgroundColor: C.surface, borderRadius: R.lg, padding: S.md, marginBottom: S.sm, ...shadow.sm },
|
|
80
|
+
cardIcon: { width: 48, height: 48, borderRadius: R.md, backgroundColor: C.white, justifyContent: 'center', alignItems: 'center', marginRight: S.md, ...shadow.sm },
|
|
81
|
+
cardEmoji: { fontSize: 24 },
|
|
82
|
+
cardText: { flex: 1 },
|
|
83
|
+
cardTitle: { fontSize: F.md, fontWeight: '700', color: C.ink },
|
|
84
|
+
cardDesc: { fontSize: F.sm, color: C.muted, marginTop: 2 },
|
|
85
|
+
arrow: { fontSize: F.xl, color: C.ghost },
|
|
86
|
+
});
|