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.
Files changed (167) hide show
  1. package/Fpay.podspec +20 -0
  2. package/LICENSE +20 -0
  3. package/README.md +37 -0
  4. package/android/build.gradle +67 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/fpay/FpayModule.kt +15 -0
  7. package/android/src/main/java/com/fpay/FpayPackage.kt +31 -0
  8. package/ios/Fpay.h +5 -0
  9. package/ios/Fpay.mm +21 -0
  10. package/lib/module/FountainPayProvider.js +18 -0
  11. package/lib/module/FountainPayProvider.js.map +1 -0
  12. package/lib/module/core/api/client.js +47 -0
  13. package/lib/module/core/api/client.js.map +1 -0
  14. package/lib/module/core/api/index.js +35 -0
  15. package/lib/module/core/api/index.js.map +1 -0
  16. package/lib/module/core/types/index.js +4 -0
  17. package/lib/module/core/types/index.js.map +1 -0
  18. package/lib/module/engine/BLEReceiverService.js +190 -0
  19. package/lib/module/engine/BLEReceiverService.js.map +1 -0
  20. package/lib/module/engine/BLESenderService.js +259 -0
  21. package/lib/module/engine/BLESenderService.js.map +1 -0
  22. package/lib/module/engine/FPEngine.js +340 -0
  23. package/lib/module/engine/FPEngine.js.map +1 -0
  24. package/lib/module/engine/NearbyUsersService.js +87 -0
  25. package/lib/module/engine/NearbyUsersService.js.map +1 -0
  26. package/lib/module/index.js +16 -0
  27. package/lib/module/index.js.map +1 -0
  28. package/lib/module/package.json +1 -0
  29. package/lib/module/ui/components/FPButton.js +71 -0
  30. package/lib/module/ui/components/FPButton.js.map +1 -0
  31. package/lib/module/ui/components/LoadingAnimation/InLoading.js +74 -0
  32. package/lib/module/ui/components/LoadingAnimation/InLoading.js.map +1 -0
  33. package/lib/module/ui/components/LoadingAnimation/index.js +82 -0
  34. package/lib/module/ui/components/LoadingAnimation/index.js.map +1 -0
  35. package/lib/module/ui/components/OtpInput/OTPInputView.js +290 -0
  36. package/lib/module/ui/components/OtpInput/OTPInputView.js.map +1 -0
  37. package/lib/module/ui/components/OtpInput/Styles.js +20 -0
  38. package/lib/module/ui/components/OtpInput/Styles.js.map +1 -0
  39. package/lib/module/ui/components/OtpInput/helpers/codeToArray.js +7 -0
  40. package/lib/module/ui/components/OtpInput/helpers/codeToArray.js.map +1 -0
  41. package/lib/module/ui/components/OtpInput/helpers/device.js +9 -0
  42. package/lib/module/ui/components/OtpInput/helpers/device.js.map +1 -0
  43. package/lib/module/ui/components/OtpInput/helpers/styles.js +17 -0
  44. package/lib/module/ui/components/OtpInput/helpers/styles.js.map +1 -0
  45. package/lib/module/ui/components/OtpInput/helpers/types.js +4 -0
  46. package/lib/module/ui/components/OtpInput/helpers/types.js.map +1 -0
  47. package/lib/module/ui/components/OtpInput/index.js +45 -0
  48. package/lib/module/ui/components/OtpInput/index.js.map +1 -0
  49. package/lib/module/ui/components/PulseAnimation.js +61 -0
  50. package/lib/module/ui/components/PulseAnimation.js.map +1 -0
  51. package/lib/module/ui/modals/FPPaymentRequestModal.js +253 -0
  52. package/lib/module/ui/modals/FPPaymentRequestModal.js.map +1 -0
  53. package/lib/module/ui/modals/FPShell.js +180 -0
  54. package/lib/module/ui/modals/FPShell.js.map +1 -0
  55. package/lib/module/ui/screens/ReceiveScreen.js +291 -0
  56. package/lib/module/ui/screens/ReceiveScreen.js.map +1 -0
  57. package/lib/module/ui/screens/SendScreen.js +216 -0
  58. package/lib/module/ui/screens/SendScreen.js.map +1 -0
  59. package/lib/module/ui/screens/sub/BluetoothSubScreen.js +403 -0
  60. package/lib/module/ui/screens/sub/BluetoothSubScreen.js.map +1 -0
  61. package/lib/module/ui/screens/sub/NFCSubScreen.js +169 -0
  62. package/lib/module/ui/screens/sub/NFCSubScreen.js.map +1 -0
  63. package/lib/module/ui/screens/sub/NQRSubScreen.js +136 -0
  64. package/lib/module/ui/screens/sub/NQRSubScreen.js.map +1 -0
  65. package/lib/module/ui/screens/sub/ProximitySubScreen.js +501 -0
  66. package/lib/module/ui/screens/sub/ProximitySubScreen.js.map +1 -0
  67. package/lib/module/ui/screens/sub/TransferSubScreen.js +361 -0
  68. package/lib/module/ui/screens/sub/TransferSubScreen.js.map +1 -0
  69. package/lib/module/ui/theme/index.js +64 -0
  70. package/lib/module/ui/theme/index.js.map +1 -0
  71. package/lib/module/useFountainPay.js +82 -0
  72. package/lib/module/useFountainPay.js.map +1 -0
  73. package/lib/typescript/package.json +1 -0
  74. package/lib/typescript/src/FountainPayProvider.d.ts +7 -0
  75. package/lib/typescript/src/FountainPayProvider.d.ts.map +1 -0
  76. package/lib/typescript/src/core/api/client.d.ts +7 -0
  77. package/lib/typescript/src/core/api/client.d.ts.map +1 -0
  78. package/lib/typescript/src/core/api/index.d.ts +67 -0
  79. package/lib/typescript/src/core/api/index.d.ts.map +1 -0
  80. package/lib/typescript/src/core/types/index.d.ts +130 -0
  81. package/lib/typescript/src/core/types/index.d.ts.map +1 -0
  82. package/lib/typescript/src/engine/BLEReceiverService.d.ts +43 -0
  83. package/lib/typescript/src/engine/BLEReceiverService.d.ts.map +1 -0
  84. package/lib/typescript/src/engine/BLESenderService.d.ts +39 -0
  85. package/lib/typescript/src/engine/BLESenderService.d.ts.map +1 -0
  86. package/lib/typescript/src/engine/FPEngine.d.ts +24 -0
  87. package/lib/typescript/src/engine/FPEngine.d.ts.map +1 -0
  88. package/lib/typescript/src/engine/NearbyUsersService.d.ts +19 -0
  89. package/lib/typescript/src/engine/NearbyUsersService.d.ts.map +1 -0
  90. package/lib/typescript/src/index.d.ts +4 -0
  91. package/lib/typescript/src/index.d.ts.map +1 -0
  92. package/lib/typescript/src/ui/components/FPButton.d.ts +12 -0
  93. package/lib/typescript/src/ui/components/FPButton.d.ts.map +1 -0
  94. package/lib/typescript/src/ui/components/LoadingAnimation/InLoading.d.ts +7 -0
  95. package/lib/typescript/src/ui/components/LoadingAnimation/InLoading.d.ts.map +1 -0
  96. package/lib/typescript/src/ui/components/LoadingAnimation/index.d.ts +6 -0
  97. package/lib/typescript/src/ui/components/LoadingAnimation/index.d.ts.map +1 -0
  98. package/lib/typescript/src/ui/components/OtpInput/OTPInputView.d.ts +29 -0
  99. package/lib/typescript/src/ui/components/OtpInput/OTPInputView.d.ts.map +1 -0
  100. package/lib/typescript/src/ui/components/OtpInput/Styles.d.ts +330 -0
  101. package/lib/typescript/src/ui/components/OtpInput/Styles.d.ts.map +1 -0
  102. package/lib/typescript/src/ui/components/OtpInput/helpers/codeToArray.d.ts +6 -0
  103. package/lib/typescript/src/ui/components/OtpInput/helpers/codeToArray.d.ts.map +1 -0
  104. package/lib/typescript/src/ui/components/OtpInput/helpers/device.d.ts +6 -0
  105. package/lib/typescript/src/ui/components/OtpInput/helpers/device.d.ts.map +1 -0
  106. package/lib/typescript/src/ui/components/OtpInput/helpers/styles.d.ts +6 -0
  107. package/lib/typescript/src/ui/components/OtpInput/helpers/styles.d.ts.map +1 -0
  108. package/lib/typescript/src/ui/components/OtpInput/helpers/types.d.ts +84 -0
  109. package/lib/typescript/src/ui/components/OtpInput/helpers/types.d.ts.map +1 -0
  110. package/lib/typescript/src/ui/components/OtpInput/index.d.ts +9 -0
  111. package/lib/typescript/src/ui/components/OtpInput/index.d.ts.map +1 -0
  112. package/lib/typescript/src/ui/components/PulseAnimation.d.ts +2 -0
  113. package/lib/typescript/src/ui/components/PulseAnimation.d.ts.map +1 -0
  114. package/lib/typescript/src/ui/modals/FPPaymentRequestModal.d.ts +2 -0
  115. package/lib/typescript/src/ui/modals/FPPaymentRequestModal.d.ts.map +1 -0
  116. package/lib/typescript/src/ui/modals/FPShell.d.ts +2 -0
  117. package/lib/typescript/src/ui/modals/FPShell.d.ts.map +1 -0
  118. package/lib/typescript/src/ui/screens/ReceiveScreen.d.ts +10 -0
  119. package/lib/typescript/src/ui/screens/ReceiveScreen.d.ts.map +1 -0
  120. package/lib/typescript/src/ui/screens/SendScreen.d.ts +9 -0
  121. package/lib/typescript/src/ui/screens/SendScreen.d.ts.map +1 -0
  122. package/lib/typescript/src/ui/screens/sub/BluetoothSubScreen.d.ts +552 -0
  123. package/lib/typescript/src/ui/screens/sub/BluetoothSubScreen.d.ts.map +1 -0
  124. package/lib/typescript/src/ui/screens/sub/NFCSubScreen.d.ts +19 -0
  125. package/lib/typescript/src/ui/screens/sub/NFCSubScreen.d.ts.map +1 -0
  126. package/lib/typescript/src/ui/screens/sub/NQRSubScreen.d.ts +13 -0
  127. package/lib/typescript/src/ui/screens/sub/NQRSubScreen.d.ts.map +1 -0
  128. package/lib/typescript/src/ui/screens/sub/ProximitySubScreen.d.ts +552 -0
  129. package/lib/typescript/src/ui/screens/sub/ProximitySubScreen.d.ts.map +1 -0
  130. package/lib/typescript/src/ui/screens/sub/TransferSubScreen.d.ts +12 -0
  131. package/lib/typescript/src/ui/screens/sub/TransferSubScreen.d.ts.map +1 -0
  132. package/lib/typescript/src/ui/theme/index.d.ts +62 -0
  133. package/lib/typescript/src/ui/theme/index.d.ts.map +1 -0
  134. package/lib/typescript/src/useFountainPay.d.ts +3 -0
  135. package/lib/typescript/src/useFountainPay.d.ts.map +1 -0
  136. package/package.json +217 -0
  137. package/src/FountainPayProvider.tsx +21 -0
  138. package/src/core/api/client.ts +47 -0
  139. package/src/core/api/index.ts +61 -0
  140. package/src/core/types/index.ts +144 -0
  141. package/src/engine/BLEReceiverService.ts +244 -0
  142. package/src/engine/BLESenderService.ts +314 -0
  143. package/src/engine/FPEngine.ts +370 -0
  144. package/src/engine/NearbyUsersService.ts +106 -0
  145. package/src/index.ts +30 -0
  146. package/src/ui/components/FPButton.tsx +42 -0
  147. package/src/ui/components/LoadingAnimation/InLoading.tsx +88 -0
  148. package/src/ui/components/LoadingAnimation/index.tsx +93 -0
  149. package/src/ui/components/OtpInput/OTPInputView.tsx +243 -0
  150. package/src/ui/components/OtpInput/Styles.ts +19 -0
  151. package/src/ui/components/OtpInput/helpers/codeToArray.ts +3 -0
  152. package/src/ui/components/OtpInput/helpers/device.ts +6 -0
  153. package/src/ui/components/OtpInput/helpers/styles.ts +17 -0
  154. package/src/ui/components/OtpInput/helpers/types.ts +88 -0
  155. package/src/ui/components/OtpInput/index.tsx +51 -0
  156. package/src/ui/components/PulseAnimation.tsx +78 -0
  157. package/src/ui/modals/FPPaymentRequestModal.tsx +158 -0
  158. package/src/ui/modals/FPShell.tsx +107 -0
  159. package/src/ui/screens/ReceiveScreen.tsx +119 -0
  160. package/src/ui/screens/SendScreen.tsx +86 -0
  161. package/src/ui/screens/sub/BluetoothSubScreen.tsx +433 -0
  162. package/src/ui/screens/sub/NFCSubScreen.tsx +83 -0
  163. package/src/ui/screens/sub/NQRSubScreen.tsx +61 -0
  164. package/src/ui/screens/sub/ProximitySubScreen.tsx +390 -0
  165. package/src/ui/screens/sub/TransferSubScreen.tsx +146 -0
  166. package/src/ui/theme/index.ts +24 -0
  167. 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
+ });