zopassport 0.1.0
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/LICENSE +22 -0
- package/README.md +407 -0
- package/app/.env.example +15 -0
- package/app/README.md +28 -0
- package/app/package.json +24 -0
- package/app/reanimated-mock.js +102 -0
- package/app/reanimated-mock.jsx +97 -0
- package/app/src/App.tsx +331 -0
- package/app/src/components/FounderBadge.tsx +26 -0
- package/app/src/components/OTPInput.tsx +149 -0
- package/app/src/components/PhoneInput.tsx +109 -0
- package/app/src/components/ZoAuth.tsx +320 -0
- package/app/src/components/ZoAvatar.tsx +87 -0
- package/app/src/components/ZoLanding.tsx +231 -0
- package/app/src/components/ZoOnboarding.tsx +524 -0
- package/app/src/components/ZoPassportCard.tsx +183 -0
- package/app/src/components/ZoProgressRing.tsx +57 -0
- package/app/src/components/index.ts +16 -0
- package/app/src/components/wallet/MovingShine.tsx +43 -0
- package/app/src/components/wallet/TransactionItem.tsx +84 -0
- package/app/src/components/wallet/TransactionList.tsx +65 -0
- package/app/src/components/wallet/WalletCard.tsx +152 -0
- package/app/src/components/wallet/WalletScreen.tsx +190 -0
- package/app/src/components/wallet/ZoToken.tsx +69 -0
- package/app/src/components/wallet/index.ts +8 -0
- package/app/src/components/wallet/styles/index.ts +4 -0
- package/app/src/components/wallet/styles/walletStyles.ts +210 -0
- package/app/src/sdk/ZoPassportSDK.ts +277 -0
- package/app/src/sdk/lib/api/auth.ts +223 -0
- package/app/src/sdk/lib/api/avatar.ts +155 -0
- package/app/src/sdk/lib/api/client.ts +135 -0
- package/app/src/sdk/lib/api/index.ts +8 -0
- package/app/src/sdk/lib/api/profile.ts +80 -0
- package/app/src/sdk/lib/api/wallet.ts +59 -0
- package/app/src/sdk/lib/types/auth.ts +78 -0
- package/app/src/sdk/lib/types/avatar.ts +22 -0
- package/app/src/sdk/lib/types/index.ts +8 -0
- package/app/src/sdk/lib/types/profile.ts +18 -0
- package/app/src/sdk/lib/types/wallet.ts +103 -0
- package/app/src/sdk/lib/types.ts +205 -0
- package/app/src/sdk/lib/utils/index.ts +6 -0
- package/app/src/sdk/lib/utils/phone.ts +71 -0
- package/app/src/sdk/lib/utils/storage.ts +116 -0
- package/app/src/sdk/lib/utils/wallet.ts +73 -0
- package/app/src/sdk/types.ts +205 -0
- package/app/src/styles.css +154 -0
- package/app/svg-mock.js +125 -0
- package/app/svg-mock.jsx +120 -0
- package/app/vite.config.ts +70 -0
- package/assets/ASSETS_MANIFEST.md +124 -0
- package/assets/bae.png +0 -0
- package/assets/bro.png +0 -0
- package/assets/cultural-stickers/Business.png +0 -0
- package/assets/cultural-stickers/Default (2).jpg +0 -0
- package/assets/cultural-stickers/Design.png +0 -0
- package/assets/cultural-stickers/FollowYourHeart.png +0 -0
- package/assets/cultural-stickers/Food.png +0 -0
- package/assets/cultural-stickers/Game.png +0 -0
- package/assets/cultural-stickers/Health&Fitness.png +0 -0
- package/assets/cultural-stickers/Home&Lifestyle.png +0 -0
- package/assets/cultural-stickers/Law.png +0 -0
- package/assets/cultural-stickers/Literature&Stories.png +0 -0
- package/assets/cultural-stickers/Music&Entertainment.png +0 -0
- package/assets/cultural-stickers/Nature&Wildlife.png +0 -0
- package/assets/cultural-stickers/Photography.png +0 -0
- package/assets/cultural-stickers/Science&Technology.png +0 -0
- package/assets/cultural-stickers/Spiritual.png +0 -0
- package/assets/cultural-stickers/Sport.png +0 -0
- package/assets/cultural-stickers/Stories&Journal.png +0 -0
- package/assets/cultural-stickers/Television&Cinema.png +0 -0
- package/assets/cultural-stickers/Travel&Adventure.png +0 -0
- package/assets/cultural-stickers/z.jpg (1).jpg +0 -0
- package/assets/figma-assets/landing-zo-logo.png +0 -0
- package/assets/images/rank1.jpeg +0 -0
- package/assets/index.ts +76 -0
- package/assets/lotties/loader.json +1216 -0
- package/assets/lotties/spinner.json +1 -0
- package/assets/videos/loading-screen-background.mp4 +0 -0
- package/assets/videos/opening-disks.mp4 +0 -0
- package/assets/wallet/constants.ts +38 -0
- package/assets/zo-coin.gif +0 -0
- package/assets/zo-fallback.png +0 -0
- package/dist/assets/index.d.mts +136 -0
- package/dist/assets/index.d.ts +136 -0
- package/dist/assets/index.js +133 -0
- package/dist/assets/index.js.map +1 -0
- package/dist/assets/index.mjs +100 -0
- package/dist/assets/index.mjs.map +1 -0
- package/dist/index.d.mts +789 -0
- package/dist/index.d.ts +789 -0
- package/dist/index.js +1118 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1060 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react-native.d.mts +537 -0
- package/dist/react-native.d.ts +537 -0
- package/dist/react-native.js +1617 -0
- package/dist/react-native.js.map +1 -0
- package/dist/react-native.mjs +1588 -0
- package/dist/react-native.mjs.map +1 -0
- package/dist/react.d.mts +824 -0
- package/dist/react.d.ts +824 -0
- package/dist/react.js +3856 -0
- package/dist/react.js.map +1 -0
- package/dist/react.mjs +3801 -0
- package/dist/react.mjs.map +1 -0
- package/package.json +112 -0
- package/scripts/init.js +196 -0
- package/scripts/postinstall.js +174 -0
- package/scripts/verify-build.js +121 -0
package/app/src/App.tsx
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { ZoPassportSDK } from 'zopassport';
|
|
3
|
+
import { ZoLanding, ZoPassportPage, WalletFullPage } from 'zopassport/react';
|
|
4
|
+
import type { ZoUser } from 'zopassport';
|
|
5
|
+
|
|
6
|
+
// Get client key from environment
|
|
7
|
+
const CLIENT_KEY = import.meta.env.VITE_ZO_CLIENT_KEY || '';
|
|
8
|
+
const API_URL = import.meta.env.VITE_ZO_API_URL;
|
|
9
|
+
|
|
10
|
+
// Validate configuration
|
|
11
|
+
if (!CLIENT_KEY || CLIENT_KEY === 'your-client-key-here') {
|
|
12
|
+
console.error(`
|
|
13
|
+
╔════════════════════════════════════════════════════════════╗
|
|
14
|
+
║ ⚠️ CONFIGURATION ERROR ║
|
|
15
|
+
╠════════════════════════════════════════════════════════════╣
|
|
16
|
+
║ Missing ZO_CLIENT_KEY! ║
|
|
17
|
+
║ ║
|
|
18
|
+
║ 1. Copy .env.example to .env ║
|
|
19
|
+
║ cp .env.example .env ║
|
|
20
|
+
║ ║
|
|
21
|
+
║ 2. Edit .env and add your client key ║
|
|
22
|
+
║ VITE_ZO_CLIENT_KEY=your-actual-key ║
|
|
23
|
+
║ ║
|
|
24
|
+
║ Get your key at: https://zo.xyz/developers ║
|
|
25
|
+
╚════════════════════════════════════════════════════════════╝
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Initialize SDK
|
|
30
|
+
const sdk = new ZoPassportSDK({
|
|
31
|
+
clientKey: CLIENT_KEY,
|
|
32
|
+
baseUrl: API_URL,
|
|
33
|
+
autoRefresh: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
type Screen = 'landing' | 'passport' | 'wallet';
|
|
37
|
+
|
|
38
|
+
function App() {
|
|
39
|
+
const [screen, setScreen] = useState<Screen>('landing');
|
|
40
|
+
const [user, setUser] = useState<ZoUser | null>(null);
|
|
41
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
42
|
+
const [walletBalance, setWalletBalance] = useState(0);
|
|
43
|
+
const [walletOpen, setWalletOpen] = useState(false);
|
|
44
|
+
const [isLoadingBalance, setIsLoadingBalance] = useState(false);
|
|
45
|
+
|
|
46
|
+
// Fetch wallet balance
|
|
47
|
+
const fetchWalletBalance = async () => {
|
|
48
|
+
setIsLoadingBalance(true);
|
|
49
|
+
try {
|
|
50
|
+
const balance = await sdk.getWalletBalance();
|
|
51
|
+
console.log('[App] Wallet balance fetched:', balance);
|
|
52
|
+
setWalletBalance(balance);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.warn('[App] Could not fetch wallet balance, using default:', error);
|
|
55
|
+
// Don't throw - just keep the existing balance
|
|
56
|
+
} finally {
|
|
57
|
+
setIsLoadingBalance(false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Check for existing session on mount
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const checkSession = async () => {
|
|
64
|
+
setIsLoading(true);
|
|
65
|
+
try {
|
|
66
|
+
if (sdk.isAuthenticated) {
|
|
67
|
+
const profile = await sdk.getProfile();
|
|
68
|
+
if (profile) {
|
|
69
|
+
setUser(profile);
|
|
70
|
+
setScreen('passport');
|
|
71
|
+
// Fetch wallet balance
|
|
72
|
+
await fetchWalletBalance();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('Session check failed:', error);
|
|
77
|
+
} finally {
|
|
78
|
+
setIsLoading(false);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
checkSession();
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
// Refresh balance when wallet opens
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (walletOpen) {
|
|
87
|
+
fetchWalletBalance();
|
|
88
|
+
}
|
|
89
|
+
}, [walletOpen]);
|
|
90
|
+
|
|
91
|
+
// Auth handlers
|
|
92
|
+
const handleSendOTP = async (countryCode: string, phoneNumber: string) => {
|
|
93
|
+
return sdk.auth.sendOTP(countryCode, phoneNumber);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const handleVerifyOTP = async (countryCode: string, phoneNumber: string, otp: string) => {
|
|
97
|
+
const result = await sdk.loginWithPhone(countryCode, phoneNumber, otp);
|
|
98
|
+
if (result.success && result.user) {
|
|
99
|
+
return { success: true, user: result.user };
|
|
100
|
+
}
|
|
101
|
+
return { success: false, error: result.error };
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const handleAuthSuccess = async (userId: string, userData: ZoUser) => {
|
|
105
|
+
setUser(userData);
|
|
106
|
+
setScreen('passport');
|
|
107
|
+
// Fetch wallet balance
|
|
108
|
+
try {
|
|
109
|
+
const balance = await sdk.getWalletBalance();
|
|
110
|
+
setWalletBalance(balance);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
console.error('Failed to fetch wallet:', e);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handleLogout = async () => {
|
|
117
|
+
await sdk.logout();
|
|
118
|
+
setUser(null);
|
|
119
|
+
setScreen('landing');
|
|
120
|
+
setWalletBalance(0);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Loading screen
|
|
124
|
+
if (isLoading) {
|
|
125
|
+
return (
|
|
126
|
+
<div style={styles.loadingContainer}>
|
|
127
|
+
<div style={styles.loader} />
|
|
128
|
+
<p style={styles.loadingText}>Loading Zo Passport...</p>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Config error screen
|
|
134
|
+
if (!CLIENT_KEY || CLIENT_KEY === 'your-client-key-here') {
|
|
135
|
+
return (
|
|
136
|
+
<div style={styles.errorContainer}>
|
|
137
|
+
<div style={styles.errorCard}>
|
|
138
|
+
<div style={styles.errorIcon}>⚠️</div>
|
|
139
|
+
<h1 style={styles.errorTitle}>Configuration Required</h1>
|
|
140
|
+
<p style={styles.errorText}>
|
|
141
|
+
Your Zo Passport needs a client key to connect to the Zo World.
|
|
142
|
+
</p>
|
|
143
|
+
<div style={styles.codeBlock}>
|
|
144
|
+
<p style={styles.codeComment}># 1. Copy the example config</p>
|
|
145
|
+
<code>cp .env.example .env</code>
|
|
146
|
+
<p style={styles.codeComment}># 2. Edit .env with your key</p>
|
|
147
|
+
<code>VITE_ZO_CLIENT_KEY=your-key</code>
|
|
148
|
+
<p style={styles.codeComment}># 3. Restart the dev server</p>
|
|
149
|
+
<code>npm run dev</code>
|
|
150
|
+
</div>
|
|
151
|
+
<a
|
|
152
|
+
href="https://zo.xyz/developers"
|
|
153
|
+
target="_blank"
|
|
154
|
+
rel="noopener noreferrer"
|
|
155
|
+
style={styles.getKeyButton}
|
|
156
|
+
>
|
|
157
|
+
Get Your Client Key →
|
|
158
|
+
</a>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Landing / Auth screen
|
|
165
|
+
if (screen === 'landing') {
|
|
166
|
+
return (
|
|
167
|
+
<ZoLanding
|
|
168
|
+
onAuthSuccess={handleAuthSuccess}
|
|
169
|
+
sendOTP={handleSendOTP}
|
|
170
|
+
verifyOTP={handleVerifyOTP}
|
|
171
|
+
/>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Main Passport screen
|
|
176
|
+
return (
|
|
177
|
+
<div style={styles.container}>
|
|
178
|
+
{/* Header */}
|
|
179
|
+
<header style={styles.header}>
|
|
180
|
+
<img
|
|
181
|
+
src="/figma-assets/landing-zo-logo.png"
|
|
182
|
+
alt="Zo"
|
|
183
|
+
style={styles.logo}
|
|
184
|
+
/>
|
|
185
|
+
<button onClick={handleLogout} style={styles.logoutButton}>
|
|
186
|
+
Logout
|
|
187
|
+
</button>
|
|
188
|
+
</header>
|
|
189
|
+
|
|
190
|
+
{/* Full Passport Page */}
|
|
191
|
+
<ZoPassportPage
|
|
192
|
+
user={user}
|
|
193
|
+
balance={walletBalance}
|
|
194
|
+
completion={{ done: 8, total: 10 }}
|
|
195
|
+
walletOpen={walletOpen}
|
|
196
|
+
onWalletToggle={() => setWalletOpen(!walletOpen)}
|
|
197
|
+
/>
|
|
198
|
+
|
|
199
|
+
{/* Wallet Full Screen */}
|
|
200
|
+
{walletOpen && (
|
|
201
|
+
<WalletFullPage
|
|
202
|
+
user={user}
|
|
203
|
+
balance={walletBalance}
|
|
204
|
+
onClose={() => setWalletOpen(false)}
|
|
205
|
+
isLoading={isLoadingBalance}
|
|
206
|
+
onRefresh={fetchWalletBalance}
|
|
207
|
+
/>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const styles: { [key: string]: React.CSSProperties } = {
|
|
214
|
+
// Loading
|
|
215
|
+
loadingContainer: {
|
|
216
|
+
display: 'flex',
|
|
217
|
+
flexDirection: 'column',
|
|
218
|
+
alignItems: 'center',
|
|
219
|
+
justifyContent: 'center',
|
|
220
|
+
height: '100vh',
|
|
221
|
+
background: '#000',
|
|
222
|
+
},
|
|
223
|
+
loader: {
|
|
224
|
+
width: '48px',
|
|
225
|
+
height: '48px',
|
|
226
|
+
border: '3px solid rgba(255,255,255,0.1)',
|
|
227
|
+
borderTopColor: '#fff',
|
|
228
|
+
borderRadius: '50%',
|
|
229
|
+
animation: 'spin 1s linear infinite',
|
|
230
|
+
},
|
|
231
|
+
loadingText: {
|
|
232
|
+
marginTop: '16px',
|
|
233
|
+
color: 'rgba(255,255,255,0.6)',
|
|
234
|
+
fontSize: '14px',
|
|
235
|
+
},
|
|
236
|
+
// Error
|
|
237
|
+
errorContainer: {
|
|
238
|
+
display: 'flex',
|
|
239
|
+
alignItems: 'center',
|
|
240
|
+
justifyContent: 'center',
|
|
241
|
+
minHeight: '100vh',
|
|
242
|
+
background: 'linear-gradient(180deg, #000 0%, #1a1a2e 100%)',
|
|
243
|
+
padding: '24px',
|
|
244
|
+
},
|
|
245
|
+
errorCard: {
|
|
246
|
+
background: 'rgba(255,255,255,0.05)',
|
|
247
|
+
border: '1px solid rgba(255,255,255,0.1)',
|
|
248
|
+
borderRadius: '24px',
|
|
249
|
+
padding: '48px',
|
|
250
|
+
maxWidth: '500px',
|
|
251
|
+
textAlign: 'center' as const,
|
|
252
|
+
},
|
|
253
|
+
errorIcon: {
|
|
254
|
+
fontSize: '64px',
|
|
255
|
+
marginBottom: '24px',
|
|
256
|
+
},
|
|
257
|
+
errorTitle: {
|
|
258
|
+
fontFamily: 'Syne, sans-serif',
|
|
259
|
+
fontSize: '28px',
|
|
260
|
+
fontWeight: 800,
|
|
261
|
+
marginBottom: '16px',
|
|
262
|
+
},
|
|
263
|
+
errorText: {
|
|
264
|
+
color: 'rgba(255,255,255,0.6)',
|
|
265
|
+
fontSize: '16px',
|
|
266
|
+
lineHeight: 1.6,
|
|
267
|
+
marginBottom: '24px',
|
|
268
|
+
},
|
|
269
|
+
codeBlock: {
|
|
270
|
+
background: 'rgba(0,0,0,0.5)',
|
|
271
|
+
borderRadius: '12px',
|
|
272
|
+
padding: '20px',
|
|
273
|
+
textAlign: 'left' as const,
|
|
274
|
+
fontFamily: 'monospace',
|
|
275
|
+
fontSize: '14px',
|
|
276
|
+
marginBottom: '24px',
|
|
277
|
+
},
|
|
278
|
+
codeComment: {
|
|
279
|
+
color: 'rgba(255,255,255,0.4)',
|
|
280
|
+
margin: '8px 0 4px 0',
|
|
281
|
+
fontSize: '12px',
|
|
282
|
+
},
|
|
283
|
+
getKeyButton: {
|
|
284
|
+
display: 'inline-block',
|
|
285
|
+
background: '#fff',
|
|
286
|
+
color: '#000',
|
|
287
|
+
padding: '16px 32px',
|
|
288
|
+
borderRadius: '12px',
|
|
289
|
+
textDecoration: 'none',
|
|
290
|
+
fontWeight: 600,
|
|
291
|
+
fontSize: '16px',
|
|
292
|
+
transition: 'transform 0.2s',
|
|
293
|
+
},
|
|
294
|
+
// Main container
|
|
295
|
+
container: {
|
|
296
|
+
minHeight: '100%',
|
|
297
|
+
background: '#000',
|
|
298
|
+
},
|
|
299
|
+
// Header
|
|
300
|
+
header: {
|
|
301
|
+
display: 'flex',
|
|
302
|
+
justifyContent: 'space-between',
|
|
303
|
+
alignItems: 'center',
|
|
304
|
+
padding: '12px 16px',
|
|
305
|
+
borderBottom: '1px solid rgba(255,255,255,0.1)',
|
|
306
|
+
background: '#000',
|
|
307
|
+
position: 'sticky' as const,
|
|
308
|
+
top: 0,
|
|
309
|
+
zIndex: 100,
|
|
310
|
+
flexShrink: 0,
|
|
311
|
+
},
|
|
312
|
+
logo: {
|
|
313
|
+
width: '36px',
|
|
314
|
+
height: '36px',
|
|
315
|
+
},
|
|
316
|
+
logoutButton: {
|
|
317
|
+
background: 'transparent',
|
|
318
|
+
border: '1px solid rgba(255,255,255,0.2)',
|
|
319
|
+
borderRadius: '8px',
|
|
320
|
+
padding: '8px 16px',
|
|
321
|
+
color: 'rgba(255,255,255,0.6)',
|
|
322
|
+
cursor: 'pointer',
|
|
323
|
+
fontSize: '14px',
|
|
324
|
+
transition: 'all 0.2s',
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export default App;
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/components/FounderBadge.tsx
|
|
2
|
+
// Founder badge SVG icon
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
export interface FounderBadgeProps {
|
|
7
|
+
/** Size in pixels (default: 32) */
|
|
8
|
+
size?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const FounderBadge: React.FC<FounderBadgeProps> = ({ size = 32 }) => (
|
|
12
|
+
<svg width={size} height={size} viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
13
|
+
<path
|
|
14
|
+
d="M12.0117 3.15234C13.1449 2.14828 14.8551 2.14828 15.9883 3.15234L16.0996 3.25684L17.7715 4.89453L20.1123 4.91895L20.2646 4.92383C21.7758 5.01516 22.9848 6.22423 23.0762 7.73535L23.0811 7.8877L23.1045 10.2275L24.7432 11.9004L24.8477 12.0117C25.8517 13.1449 25.8517 14.8551 24.8477 15.9883L24.7432 16.0996L23.1045 17.7715L23.0811 20.1123C23.0646 21.6938 21.8262 22.9818 20.2646 23.0762L20.1123 23.0811L17.7715 23.1045L16.0996 24.7432C14.9697 25.8498 13.1826 25.8852 12.0117 24.8477L11.9004 24.7432L10.2275 23.1045L7.8877 23.0811C6.30625 23.0646 5.01821 21.8262 4.92383 20.2646L4.91895 20.1123L4.89453 17.7715L3.25684 16.0996C2.11446 14.9333 2.11446 13.0667 3.25684 11.9004L4.89453 10.2275L4.91895 7.8877L4.92383 7.73535C5.01821 6.17382 6.30624 4.93536 7.8877 4.91895L10.2275 4.89453L11.9004 3.25684L12.0117 3.15234Z"
|
|
15
|
+
fill="#FF2F8E"
|
|
16
|
+
stroke="#111111"
|
|
17
|
+
strokeWidth="4"
|
|
18
|
+
strokeLinejoin="round"
|
|
19
|
+
/>
|
|
20
|
+
<path
|
|
21
|
+
d="M13.5008 16.1741H15.8997C16.4443 16.1741 16.8858 16.6156 16.8858 17.1602C16.8858 17.7048 16.4443 18.1463 15.8997 18.1463H12.2286C11.4558 18.1463 10.8293 17.5199 10.8293 16.7471C10.8293 16.4219 10.9425 16.1069 11.1495 15.8562L14.0743 12.3137H11.8434C11.2988 12.3137 10.8573 11.8722 10.8573 11.3276C10.8573 10.783 11.2988 10.3415 11.8434 10.3415H15.4226C16.1921 10.3415 16.8158 10.9652 16.8158 11.7347C16.8158 12.0634 16.6996 12.3816 16.4876 12.6329L13.5008 16.1741Z"
|
|
22
|
+
fill="white"
|
|
23
|
+
/>
|
|
24
|
+
</svg>
|
|
25
|
+
);
|
|
26
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// src/components/OTPInput.tsx
|
|
2
|
+
// 6-digit OTP input component
|
|
3
|
+
|
|
4
|
+
import React, { useRef, useEffect } from 'react';
|
|
5
|
+
|
|
6
|
+
export interface OTPInputProps {
|
|
7
|
+
/** Current OTP value array */
|
|
8
|
+
value: string[];
|
|
9
|
+
/** Callback when OTP changes */
|
|
10
|
+
onChange: (otp: string[]) => void;
|
|
11
|
+
/** Callback when all 6 digits entered */
|
|
12
|
+
onComplete?: (otp: string) => void;
|
|
13
|
+
/** Number of digits (default: 6) */
|
|
14
|
+
length?: number;
|
|
15
|
+
/** Disabled state */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Error message */
|
|
18
|
+
error?: string;
|
|
19
|
+
/** Auto-focus first input */
|
|
20
|
+
autoFocus?: boolean;
|
|
21
|
+
/** Additional CSS class */
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const OTPInput: React.FC<OTPInputProps> = ({
|
|
26
|
+
value,
|
|
27
|
+
onChange,
|
|
28
|
+
onComplete,
|
|
29
|
+
length = 6,
|
|
30
|
+
disabled = false,
|
|
31
|
+
error,
|
|
32
|
+
autoFocus = true,
|
|
33
|
+
className = '',
|
|
34
|
+
}) => {
|
|
35
|
+
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
|
36
|
+
|
|
37
|
+
// Initialize refs array
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
inputRefs.current = inputRefs.current.slice(0, length);
|
|
40
|
+
}, [length]);
|
|
41
|
+
|
|
42
|
+
// Auto-focus first input
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (autoFocus && inputRefs.current[0]) {
|
|
45
|
+
setTimeout(() => inputRefs.current[0]?.focus(), 100);
|
|
46
|
+
}
|
|
47
|
+
}, [autoFocus]);
|
|
48
|
+
|
|
49
|
+
const handleChange = (index: number, inputValue: string) => {
|
|
50
|
+
// Only allow digits
|
|
51
|
+
if (inputValue && !/^\d$/.test(inputValue)) return;
|
|
52
|
+
|
|
53
|
+
const newOtp = [...value];
|
|
54
|
+
newOtp[index] = inputValue;
|
|
55
|
+
onChange(newOtp);
|
|
56
|
+
|
|
57
|
+
// Auto-focus next input
|
|
58
|
+
if (inputValue && index < length - 1) {
|
|
59
|
+
inputRefs.current[index + 1]?.focus();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check if complete
|
|
63
|
+
if (newOtp.every(digit => digit !== '') && index === length - 1) {
|
|
64
|
+
onComplete?.(newOtp.join(''));
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleKeyDown = (index: number, e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
69
|
+
// Backspace: move to previous input
|
|
70
|
+
if (e.key === 'Backspace' && !value[index] && index > 0) {
|
|
71
|
+
inputRefs.current[index - 1]?.focus();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handlePaste = (e: React.ClipboardEvent) => {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
const pastedData = e.clipboardData.getData('text').slice(0, length);
|
|
78
|
+
const digits = pastedData.replace(/\D/g, '').split('');
|
|
79
|
+
|
|
80
|
+
if (digits.length > 0) {
|
|
81
|
+
const newOtp = [...value];
|
|
82
|
+
digits.forEach((digit, i) => {
|
|
83
|
+
if (i < length) newOtp[i] = digit;
|
|
84
|
+
});
|
|
85
|
+
onChange(newOtp);
|
|
86
|
+
|
|
87
|
+
// Focus last filled input or the one after
|
|
88
|
+
const focusIndex = Math.min(digits.length, length - 1);
|
|
89
|
+
inputRefs.current[focusIndex]?.focus();
|
|
90
|
+
|
|
91
|
+
if (newOtp.every(digit => digit !== '')) {
|
|
92
|
+
onComplete?.(newOtp.join(''));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className={`zo-otp-input ${className}`}>
|
|
99
|
+
<div style={{ display: 'flex', gap: '8px', justifyContent: 'center' }}>
|
|
100
|
+
{Array.from({ length }).map((_, index) => (
|
|
101
|
+
<input
|
|
102
|
+
key={index}
|
|
103
|
+
ref={(el) => { inputRefs.current[index] = el; }}
|
|
104
|
+
type="text"
|
|
105
|
+
inputMode="numeric"
|
|
106
|
+
maxLength={1}
|
|
107
|
+
value={value[index] || ''}
|
|
108
|
+
onChange={(e) => handleChange(index, e.target.value)}
|
|
109
|
+
onKeyDown={(e) => handleKeyDown(index, e)}
|
|
110
|
+
onPaste={handlePaste}
|
|
111
|
+
disabled={disabled}
|
|
112
|
+
style={{
|
|
113
|
+
width: '48px',
|
|
114
|
+
height: '56px',
|
|
115
|
+
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
116
|
+
border: error
|
|
117
|
+
? '1px solid #ef4444'
|
|
118
|
+
: '1px solid rgba(255, 255, 255, 0.2)',
|
|
119
|
+
borderRadius: '8px',
|
|
120
|
+
textAlign: 'center',
|
|
121
|
+
color: 'white',
|
|
122
|
+
fontFamily: 'Rubik, system-ui, sans-serif',
|
|
123
|
+
fontSize: '20px',
|
|
124
|
+
fontWeight: 600,
|
|
125
|
+
cursor: disabled ? 'not-allowed' : 'text',
|
|
126
|
+
opacity: disabled ? 0.5 : 1,
|
|
127
|
+
}}
|
|
128
|
+
/>
|
|
129
|
+
))}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Error Message */}
|
|
133
|
+
{error && (
|
|
134
|
+
<p
|
|
135
|
+
style={{
|
|
136
|
+
color: '#ef4444',
|
|
137
|
+
fontSize: '14px',
|
|
138
|
+
marginTop: '8px',
|
|
139
|
+
textAlign: 'center',
|
|
140
|
+
fontFamily: 'Rubik, system-ui, sans-serif',
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
{error}
|
|
144
|
+
</p>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/components/PhoneInput.tsx
|
|
2
|
+
// Phone number input with country code selector
|
|
3
|
+
|
|
4
|
+
import React, { useState } from 'react';
|
|
5
|
+
import { COUNTRY_CODES, parsePhoneNumber } from '../lib/utils/phone';
|
|
6
|
+
|
|
7
|
+
export interface PhoneInputProps {
|
|
8
|
+
/** Current phone number value */
|
|
9
|
+
value: string;
|
|
10
|
+
/** Current country code (without +) */
|
|
11
|
+
countryCode: string;
|
|
12
|
+
/** Callback when phone number changes */
|
|
13
|
+
onChange: (phone: string) => void;
|
|
14
|
+
/** Callback when country code changes */
|
|
15
|
+
onCountryChange: (code: string) => void;
|
|
16
|
+
/** Placeholder text */
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
/** Disabled state */
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
/** Error message */
|
|
21
|
+
error?: string;
|
|
22
|
+
/** Additional CSS class */
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const PhoneInput: React.FC<PhoneInputProps> = ({
|
|
27
|
+
value,
|
|
28
|
+
countryCode,
|
|
29
|
+
onChange,
|
|
30
|
+
onCountryChange,
|
|
31
|
+
placeholder = '555-123-4567',
|
|
32
|
+
disabled = false,
|
|
33
|
+
error,
|
|
34
|
+
className = '',
|
|
35
|
+
}) => {
|
|
36
|
+
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
37
|
+
const digits = parsePhoneNumber(e.target.value);
|
|
38
|
+
onChange(digits);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className={`zo-phone-input ${className}`}>
|
|
43
|
+
<div style={{ display: 'flex', gap: '8px' }}>
|
|
44
|
+
{/* Country Code Selector */}
|
|
45
|
+
<select
|
|
46
|
+
value={countryCode}
|
|
47
|
+
onChange={(e) => onCountryChange(e.target.value)}
|
|
48
|
+
disabled={disabled}
|
|
49
|
+
style={{
|
|
50
|
+
flex: 1,
|
|
51
|
+
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
52
|
+
border: '1px solid rgba(255, 255, 255, 0.2)',
|
|
53
|
+
borderRadius: '8px',
|
|
54
|
+
padding: '12px 16px',
|
|
55
|
+
color: 'white',
|
|
56
|
+
fontFamily: 'Rubik, system-ui, sans-serif',
|
|
57
|
+
fontSize: '14px',
|
|
58
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
59
|
+
opacity: disabled ? 0.5 : 1,
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{COUNTRY_CODES.map((country) => (
|
|
63
|
+
<option key={country.code} value={country.code} style={{ backgroundColor: 'black' }}>
|
|
64
|
+
{country.flag} +{country.code}
|
|
65
|
+
</option>
|
|
66
|
+
))}
|
|
67
|
+
</select>
|
|
68
|
+
|
|
69
|
+
{/* Phone Number Input */}
|
|
70
|
+
<input
|
|
71
|
+
type="tel"
|
|
72
|
+
value={value}
|
|
73
|
+
onChange={handlePhoneChange}
|
|
74
|
+
placeholder={placeholder}
|
|
75
|
+
disabled={disabled}
|
|
76
|
+
style={{
|
|
77
|
+
flex: 2,
|
|
78
|
+
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
79
|
+
border: error
|
|
80
|
+
? '1px solid #ef4444'
|
|
81
|
+
: '1px solid rgba(255, 255, 255, 0.2)',
|
|
82
|
+
borderRadius: '8px',
|
|
83
|
+
padding: '12px 16px',
|
|
84
|
+
color: 'white',
|
|
85
|
+
fontFamily: 'Rubik, system-ui, sans-serif',
|
|
86
|
+
fontSize: '14px',
|
|
87
|
+
cursor: disabled ? 'not-allowed' : 'text',
|
|
88
|
+
opacity: disabled ? 0.5 : 1,
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Error Message */}
|
|
94
|
+
{error && (
|
|
95
|
+
<p
|
|
96
|
+
style={{
|
|
97
|
+
color: '#ef4444',
|
|
98
|
+
fontSize: '14px',
|
|
99
|
+
marginTop: '8px',
|
|
100
|
+
fontFamily: 'Rubik, system-ui, sans-serif',
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
{error}
|
|
104
|
+
</p>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|