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.
Files changed (110) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +407 -0
  3. package/app/.env.example +15 -0
  4. package/app/README.md +28 -0
  5. package/app/package.json +24 -0
  6. package/app/reanimated-mock.js +102 -0
  7. package/app/reanimated-mock.jsx +97 -0
  8. package/app/src/App.tsx +331 -0
  9. package/app/src/components/FounderBadge.tsx +26 -0
  10. package/app/src/components/OTPInput.tsx +149 -0
  11. package/app/src/components/PhoneInput.tsx +109 -0
  12. package/app/src/components/ZoAuth.tsx +320 -0
  13. package/app/src/components/ZoAvatar.tsx +87 -0
  14. package/app/src/components/ZoLanding.tsx +231 -0
  15. package/app/src/components/ZoOnboarding.tsx +524 -0
  16. package/app/src/components/ZoPassportCard.tsx +183 -0
  17. package/app/src/components/ZoProgressRing.tsx +57 -0
  18. package/app/src/components/index.ts +16 -0
  19. package/app/src/components/wallet/MovingShine.tsx +43 -0
  20. package/app/src/components/wallet/TransactionItem.tsx +84 -0
  21. package/app/src/components/wallet/TransactionList.tsx +65 -0
  22. package/app/src/components/wallet/WalletCard.tsx +152 -0
  23. package/app/src/components/wallet/WalletScreen.tsx +190 -0
  24. package/app/src/components/wallet/ZoToken.tsx +69 -0
  25. package/app/src/components/wallet/index.ts +8 -0
  26. package/app/src/components/wallet/styles/index.ts +4 -0
  27. package/app/src/components/wallet/styles/walletStyles.ts +210 -0
  28. package/app/src/sdk/ZoPassportSDK.ts +277 -0
  29. package/app/src/sdk/lib/api/auth.ts +223 -0
  30. package/app/src/sdk/lib/api/avatar.ts +155 -0
  31. package/app/src/sdk/lib/api/client.ts +135 -0
  32. package/app/src/sdk/lib/api/index.ts +8 -0
  33. package/app/src/sdk/lib/api/profile.ts +80 -0
  34. package/app/src/sdk/lib/api/wallet.ts +59 -0
  35. package/app/src/sdk/lib/types/auth.ts +78 -0
  36. package/app/src/sdk/lib/types/avatar.ts +22 -0
  37. package/app/src/sdk/lib/types/index.ts +8 -0
  38. package/app/src/sdk/lib/types/profile.ts +18 -0
  39. package/app/src/sdk/lib/types/wallet.ts +103 -0
  40. package/app/src/sdk/lib/types.ts +205 -0
  41. package/app/src/sdk/lib/utils/index.ts +6 -0
  42. package/app/src/sdk/lib/utils/phone.ts +71 -0
  43. package/app/src/sdk/lib/utils/storage.ts +116 -0
  44. package/app/src/sdk/lib/utils/wallet.ts +73 -0
  45. package/app/src/sdk/types.ts +205 -0
  46. package/app/src/styles.css +154 -0
  47. package/app/svg-mock.js +125 -0
  48. package/app/svg-mock.jsx +120 -0
  49. package/app/vite.config.ts +70 -0
  50. package/assets/ASSETS_MANIFEST.md +124 -0
  51. package/assets/bae.png +0 -0
  52. package/assets/bro.png +0 -0
  53. package/assets/cultural-stickers/Business.png +0 -0
  54. package/assets/cultural-stickers/Default (2).jpg +0 -0
  55. package/assets/cultural-stickers/Design.png +0 -0
  56. package/assets/cultural-stickers/FollowYourHeart.png +0 -0
  57. package/assets/cultural-stickers/Food.png +0 -0
  58. package/assets/cultural-stickers/Game.png +0 -0
  59. package/assets/cultural-stickers/Health&Fitness.png +0 -0
  60. package/assets/cultural-stickers/Home&Lifestyle.png +0 -0
  61. package/assets/cultural-stickers/Law.png +0 -0
  62. package/assets/cultural-stickers/Literature&Stories.png +0 -0
  63. package/assets/cultural-stickers/Music&Entertainment.png +0 -0
  64. package/assets/cultural-stickers/Nature&Wildlife.png +0 -0
  65. package/assets/cultural-stickers/Photography.png +0 -0
  66. package/assets/cultural-stickers/Science&Technology.png +0 -0
  67. package/assets/cultural-stickers/Spiritual.png +0 -0
  68. package/assets/cultural-stickers/Sport.png +0 -0
  69. package/assets/cultural-stickers/Stories&Journal.png +0 -0
  70. package/assets/cultural-stickers/Television&Cinema.png +0 -0
  71. package/assets/cultural-stickers/Travel&Adventure.png +0 -0
  72. package/assets/cultural-stickers/z.jpg (1).jpg +0 -0
  73. package/assets/figma-assets/landing-zo-logo.png +0 -0
  74. package/assets/images/rank1.jpeg +0 -0
  75. package/assets/index.ts +76 -0
  76. package/assets/lotties/loader.json +1216 -0
  77. package/assets/lotties/spinner.json +1 -0
  78. package/assets/videos/loading-screen-background.mp4 +0 -0
  79. package/assets/videos/opening-disks.mp4 +0 -0
  80. package/assets/wallet/constants.ts +38 -0
  81. package/assets/zo-coin.gif +0 -0
  82. package/assets/zo-fallback.png +0 -0
  83. package/dist/assets/index.d.mts +136 -0
  84. package/dist/assets/index.d.ts +136 -0
  85. package/dist/assets/index.js +133 -0
  86. package/dist/assets/index.js.map +1 -0
  87. package/dist/assets/index.mjs +100 -0
  88. package/dist/assets/index.mjs.map +1 -0
  89. package/dist/index.d.mts +789 -0
  90. package/dist/index.d.ts +789 -0
  91. package/dist/index.js +1118 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/index.mjs +1060 -0
  94. package/dist/index.mjs.map +1 -0
  95. package/dist/react-native.d.mts +537 -0
  96. package/dist/react-native.d.ts +537 -0
  97. package/dist/react-native.js +1617 -0
  98. package/dist/react-native.js.map +1 -0
  99. package/dist/react-native.mjs +1588 -0
  100. package/dist/react-native.mjs.map +1 -0
  101. package/dist/react.d.mts +824 -0
  102. package/dist/react.d.ts +824 -0
  103. package/dist/react.js +3856 -0
  104. package/dist/react.js.map +1 -0
  105. package/dist/react.mjs +3801 -0
  106. package/dist/react.mjs.map +1 -0
  107. package/package.json +112 -0
  108. package/scripts/init.js +196 -0
  109. package/scripts/postinstall.js +174 -0
  110. package/scripts/verify-build.js +121 -0
@@ -0,0 +1,210 @@
1
+ // Wallet Styles - Extracted from Zostel app
2
+ import { StyleSheet } from 'react-native';
3
+ import { WALLET_COLORS, WALLET_DIMENSIONS } from '../../../../assets/wallet/constants';
4
+
5
+ export const walletStyles = StyleSheet.create({
6
+ // Layout
7
+ flex: {
8
+ flex: 1,
9
+ },
10
+ screen: {
11
+ flex: 1,
12
+ backgroundColor: WALLET_COLORS.background,
13
+ },
14
+ container: {
15
+ flexDirection: 'column-reverse',
16
+ },
17
+
18
+ // Header
19
+ header: {
20
+ width: '100%',
21
+ flexDirection: 'row',
22
+ paddingVertical: 16,
23
+ paddingHorizontal: 24,
24
+ alignItems: 'flex-start',
25
+ },
26
+ titleContainer: {
27
+ ...StyleSheet.absoluteFillObject,
28
+ alignItems: 'center',
29
+ justifyContent: 'center',
30
+ },
31
+
32
+ // Wallet Card
33
+ cardPressContainer: {
34
+ padding: 24,
35
+ paddingBottom: 0,
36
+ paddingTop: 8,
37
+ },
38
+ card: {
39
+ aspectRatio: WALLET_DIMENSIONS.cardAspectRatio,
40
+ width: '100%',
41
+ borderRadius: WALLET_DIMENSIONS.cardBorderRadius,
42
+ backgroundColor: WALLET_COLORS.cardBackground,
43
+ },
44
+ cardContainer: {
45
+ margin: 24,
46
+ flex: 1,
47
+ alignSelf: 'stretch',
48
+ backgroundColor: WALLET_COLORS.cardInner,
49
+ borderRadius: WALLET_DIMENSIONS.innerBorderRadius,
50
+ paddingBottom: 4,
51
+ },
52
+ cardContent: {
53
+ flex: 1,
54
+ padding: 16,
55
+ backgroundColor: WALLET_COLORS.cardContent,
56
+ borderRadius: WALLET_DIMENSIONS.innerBorderRadius,
57
+ borderWidth: 1,
58
+ borderColor: WALLET_COLORS.cardBorder,
59
+ },
60
+ cardShadow: {
61
+ width: '80%',
62
+ height: 24,
63
+ backgroundColor: WALLET_COLORS.shadowDark,
64
+ position: 'absolute',
65
+ top: '45%',
66
+ left: '10%',
67
+ shadowColor: 'black',
68
+ shadowOffset: { width: 0, height: -10 },
69
+ shadowOpacity: 0.75,
70
+ shadowRadius: 16,
71
+ elevation: 5,
72
+ },
73
+
74
+ // Balance
75
+ balanceRow: {
76
+ flexDirection: 'row',
77
+ alignItems: 'center',
78
+ justifyContent: 'space-between',
79
+ },
80
+ balanceWrapper: {
81
+ flexDirection: 'row',
82
+ alignItems: 'baseline',
83
+ gap: 4,
84
+ },
85
+
86
+ // User Info
87
+ avatarInfo: {
88
+ flexDirection: 'row',
89
+ gap: 8,
90
+ },
91
+
92
+ // Card Cover
93
+ cardCover: {
94
+ aspectRatio: WALLET_DIMENSIONS.coverAspectRatio,
95
+ width: '100%',
96
+ position: 'absolute',
97
+ bottom: 0,
98
+ left: 0,
99
+ right: 0,
100
+ borderBottomLeftRadius: WALLET_DIMENSIONS.innerBorderRadius,
101
+ borderBottomRightRadius: WALLET_DIMENSIONS.innerBorderRadius,
102
+ overflow: 'hidden',
103
+ },
104
+ cardCoverTextContainer: {
105
+ position: 'absolute',
106
+ bottom: 0,
107
+ left: 0,
108
+ right: 0,
109
+ top: 0,
110
+ justifyContent: 'center',
111
+ alignItems: 'center',
112
+ },
113
+
114
+ // Shine Effect
115
+ shineContainer: {
116
+ ...StyleSheet.absoluteFillObject,
117
+ overflow: 'hidden',
118
+ },
119
+ shineEffect: {
120
+ position: 'absolute',
121
+ width: 150,
122
+ left: -60,
123
+ top: -140,
124
+ bottom: 0,
125
+ },
126
+
127
+ // Text Styles
128
+ whiteText: {
129
+ color: WALLET_COLORS.textWhite,
130
+ },
131
+ grayText: {
132
+ color: WALLET_COLORS.textGray,
133
+ },
134
+
135
+ // Token
136
+ tokenVideo: {
137
+ width: WALLET_DIMENSIONS.tokenVideoSize,
138
+ height: WALLET_DIMENSIONS.tokenVideoSize,
139
+ borderRadius: WALLET_DIMENSIONS.tokenVideoSize,
140
+ overflow: 'hidden',
141
+ },
142
+ token: {
143
+ width: WALLET_DIMENSIONS.tokenSize,
144
+ height: WALLET_DIMENSIONS.tokenSize,
145
+ },
146
+ tokenContainer: {
147
+ flexDirection: 'row',
148
+ gap: 4,
149
+ alignItems: 'center',
150
+ justifyContent: 'center',
151
+ },
152
+ textShadow: {
153
+ shadowRadius: 8,
154
+ shadowOpacity: 1,
155
+ },
156
+
157
+ // Transactions
158
+ txnContent: {
159
+ alignSelf: 'stretch',
160
+ paddingHorizontal: 24,
161
+ gap: 32,
162
+ paddingBottom: 24,
163
+ paddingTop: 40,
164
+ },
165
+ txnRow: {
166
+ minHeight: 40,
167
+ flexDirection: 'row',
168
+ gap: 12,
169
+ justifyContent: 'space-between',
170
+ alignItems: 'center',
171
+ },
172
+ iconBgTilted: {
173
+ transform: [{ rotate: '-45deg' }],
174
+ width: 40,
175
+ height: 40,
176
+ borderRadius: 20,
177
+ backgroundColor: '#202020',
178
+ alignItems: 'center',
179
+ justifyContent: 'center',
180
+ },
181
+
182
+ // States
183
+ loader: {
184
+ flex: 1,
185
+ alignItems: 'center',
186
+ justifyContent: 'center',
187
+ marginTop: 240,
188
+ },
189
+ openBg: {
190
+ ...StyleSheet.absoluteFillObject,
191
+ backgroundColor: WALLET_COLORS.background,
192
+ opacity: 0.8,
193
+ },
194
+ zoDescriptionContainer: {
195
+ position: 'absolute',
196
+ bottom: 140,
197
+ },
198
+ description: {
199
+ paddingHorizontal: 24,
200
+ color: WALLET_COLORS.textWhite,
201
+ },
202
+
203
+ // Spacing
204
+ bar: {
205
+ height: 56,
206
+ },
207
+ });
208
+
209
+ export default walletStyles;
210
+
@@ -0,0 +1,277 @@
1
+ // ZoPassportSDK - Main SDK class
2
+ // One-stop initialization for the entire Zo Passport experience
3
+
4
+ import { ZoApiClient, ZoPassportConfig } from './lib/api/client';
5
+ import { ZoAuth } from './lib/api/auth';
6
+ import { ZoProfile } from './lib/api/profile';
7
+ import { ZoAvatar } from './lib/api/avatar';
8
+ import { ZoWallet } from './lib/api/wallet';
9
+ import { LocalStorageAdapter, AsyncStorageAdapter, StorageAdapter, STORAGE_KEYS } from './lib/utils/storage';
10
+ import type { ZoUser, ZoAuthResponse, Transaction } from './lib/types';
11
+
12
+ export interface ZoPassportSDKConfig extends ZoPassportConfig {
13
+ /** Optional: Provide a custom storage adapter (default: LocalStorageAdapter) */
14
+ storageAdapter?: StorageAdapter;
15
+ /** Optional: Enable auto token refresh (default: true) */
16
+ autoRefresh?: boolean;
17
+ /** Optional: Token refresh interval in ms (default: 60000 = 1 minute) */
18
+ refreshInterval?: number;
19
+ }
20
+
21
+ export class ZoPassportSDK {
22
+ private client: ZoApiClient;
23
+ private storage: StorageAdapter;
24
+ private refreshTimer: ReturnType<typeof setTimeout> | null = null;
25
+
26
+ public auth: ZoAuth;
27
+ public profile: ZoProfile;
28
+ public avatar: ZoAvatar;
29
+ public wallet: ZoWallet;
30
+
31
+ private _user: ZoUser | null = null;
32
+ private _isAuthenticated: boolean = false;
33
+
34
+ constructor(config: ZoPassportSDKConfig) {
35
+ // Initialize storage adapter
36
+ this.storage = config.storageAdapter || new LocalStorageAdapter();
37
+
38
+ // Initialize API client
39
+ this.client = new ZoApiClient({
40
+ ...config,
41
+ storageAdapter: this.storage,
42
+ });
43
+
44
+ // Initialize API modules
45
+ this.auth = new ZoAuth(this.client, this.storage);
46
+ this.profile = new ZoProfile(this.client, this.storage);
47
+ this.avatar = new ZoAvatar(this.client, this.storage);
48
+ this.wallet = new ZoWallet(this.client);
49
+
50
+ // Start auto-refresh if enabled
51
+ if (config.autoRefresh !== false) {
52
+ this.startAutoRefresh(config.refreshInterval || 60000);
53
+ }
54
+
55
+ // Load existing session
56
+ this.loadSession();
57
+ }
58
+
59
+ // =====================
60
+ // Session Management
61
+ // =====================
62
+
63
+ private async loadSession(): Promise<void> {
64
+ try {
65
+ const userJson = await this.storage.getItem(STORAGE_KEYS.USER);
66
+ const accessToken = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
67
+
68
+ if (userJson && accessToken) {
69
+ this._user = JSON.parse(userJson);
70
+ this._isAuthenticated = true;
71
+ }
72
+ } catch (error) {
73
+ console.warn('[ZoPassport] Failed to load session:', error);
74
+ }
75
+ }
76
+
77
+ private async saveSession(authResponse: ZoAuthResponse): Promise<void> {
78
+ await this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, authResponse.access_token);
79
+ await this.storage.setItem(STORAGE_KEYS.REFRESH_TOKEN, authResponse.refresh_token);
80
+ await this.storage.setItem(STORAGE_KEYS.TOKEN_EXPIRY, authResponse.access_token_expiry);
81
+ await this.storage.setItem(STORAGE_KEYS.REFRESH_EXPIRY, authResponse.refresh_token_expiry);
82
+ await this.storage.setItem(STORAGE_KEYS.USER, JSON.stringify(authResponse.user));
83
+ await this.storage.setItem(STORAGE_KEYS.CLIENT_DEVICE_ID, authResponse.device_id || '');
84
+ await this.storage.setItem(STORAGE_KEYS.CLIENT_DEVICE_SECRET, authResponse.device_secret || '');
85
+
86
+ this._user = authResponse.user;
87
+ this._isAuthenticated = true;
88
+ }
89
+
90
+ async clearSession(): Promise<void> {
91
+ await this.storage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
92
+ await this.storage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
93
+ await this.storage.removeItem(STORAGE_KEYS.TOKEN_EXPIRY);
94
+ await this.storage.removeItem(STORAGE_KEYS.REFRESH_EXPIRY);
95
+ await this.storage.removeItem(STORAGE_KEYS.USER);
96
+ await this.storage.removeItem(STORAGE_KEYS.CLIENT_DEVICE_ID);
97
+ await this.storage.removeItem(STORAGE_KEYS.CLIENT_DEVICE_SECRET);
98
+
99
+ this._user = null;
100
+ this._isAuthenticated = false;
101
+ }
102
+
103
+ // =====================
104
+ // Auto Token Refresh
105
+ // =====================
106
+
107
+ private startAutoRefresh(interval: number): void {
108
+ this.refreshTimer = setInterval(async () => {
109
+ await this.refreshTokenIfNeeded();
110
+ }, interval);
111
+ }
112
+
113
+ private stopAutoRefresh(): void {
114
+ if (this.refreshTimer) {
115
+ clearInterval(this.refreshTimer);
116
+ this.refreshTimer = null;
117
+ }
118
+ }
119
+
120
+ private async refreshTokenIfNeeded(): Promise<void> {
121
+ const tokenExpiry = await this.storage.getItem(STORAGE_KEYS.TOKEN_EXPIRY);
122
+ const refreshToken = await this.storage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
123
+
124
+ if (!tokenExpiry || !refreshToken) return;
125
+
126
+ const expiryDate = new Date(tokenExpiry);
127
+ const now = new Date();
128
+ const twoMinutes = 2 * 60 * 1000;
129
+
130
+ // Refresh if expiring within 2 minutes
131
+ if (expiryDate.getTime() - now.getTime() < twoMinutes) {
132
+ const result = await this.auth.refreshAccessToken(refreshToken);
133
+ if (result.success && result.tokens) {
134
+ await this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, result.tokens.access);
135
+ await this.storage.setItem(STORAGE_KEYS.REFRESH_TOKEN, result.tokens.refresh);
136
+ await this.storage.setItem(STORAGE_KEYS.TOKEN_EXPIRY, result.tokens.access_expiry);
137
+ await this.storage.setItem(STORAGE_KEYS.REFRESH_EXPIRY, result.tokens.refresh_expiry);
138
+ }
139
+ }
140
+ }
141
+
142
+ // =====================
143
+ // Public API
144
+ // =====================
145
+
146
+ get user(): ZoUser | null {
147
+ return this._user;
148
+ }
149
+
150
+ get isAuthenticated(): boolean {
151
+ return this._isAuthenticated;
152
+ }
153
+
154
+ /**
155
+ * Complete phone authentication flow
156
+ */
157
+ async loginWithPhone(
158
+ countryCode: string,
159
+ phoneNumber: string,
160
+ otp: string
161
+ ): Promise<{ success: boolean; user?: ZoUser; error?: string }> {
162
+ const result = await this.auth.verifyOTP(countryCode, phoneNumber, otp);
163
+
164
+ if (result.success && result.data) {
165
+ await this.saveSession(result.data);
166
+ return { success: true, user: result.data.user };
167
+ }
168
+
169
+ return { success: false, error: result.error };
170
+ }
171
+
172
+ /**
173
+ * Logout and clear session
174
+ */
175
+ async logout(): Promise<void> {
176
+ await this.clearSession();
177
+ this.stopAutoRefresh();
178
+ }
179
+
180
+ /**
181
+ * Get current user profile
182
+ */
183
+ async getProfile(): Promise<ZoUser | null> {
184
+ const accessToken = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
185
+ if (!accessToken) return null;
186
+
187
+ const result = await this.profile.getProfile(accessToken);
188
+ if (result.success && result.profile) {
189
+ this._user = result.profile as ZoUser;
190
+ await this.storage.setItem(STORAGE_KEYS.USER, JSON.stringify(result.profile));
191
+ return result.profile as ZoUser;
192
+ }
193
+
194
+ return null;
195
+ }
196
+
197
+ /**
198
+ * Update user profile
199
+ */
200
+ async updateProfile(updates: {
201
+ first_name?: string;
202
+ last_name?: string;
203
+ bio?: string;
204
+ date_of_birth?: string;
205
+ place_name?: string;
206
+ body_type?: 'bro' | 'bae';
207
+ }): Promise<{ success: boolean; profile?: ZoUser; error?: string }> {
208
+ const accessToken = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
209
+ if (!accessToken) return { success: false, error: 'Not authenticated' };
210
+
211
+ const result = await this.profile.updateProfile(accessToken, updates);
212
+ if (result.success && result.profile) {
213
+ this._user = result.profile as ZoUser;
214
+ await this.storage.setItem(STORAGE_KEYS.USER, JSON.stringify(result.profile));
215
+ return { success: true, profile: result.profile as ZoUser };
216
+ }
217
+
218
+ return { success: false, error: result.error };
219
+ }
220
+
221
+ /**
222
+ * Generate avatar
223
+ */
224
+ async generateAvatar(bodyType: 'bro' | 'bae'): Promise<{
225
+ success: boolean;
226
+ avatarUrl?: string;
227
+ error?: string;
228
+ }> {
229
+ const accessToken = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
230
+ if (!accessToken) return { success: false, error: 'Not authenticated' };
231
+
232
+ // Start generation
233
+ const startResult = await this.avatar.generateAvatar(accessToken, bodyType);
234
+ if (!startResult.success || !startResult.task_id) {
235
+ return { success: false, error: startResult.error };
236
+ }
237
+
238
+ // Poll for completion
239
+ return new Promise((resolve) => {
240
+ this.avatar.pollAvatarStatus(accessToken, startResult.task_id!, {
241
+ onComplete: (avatarUrl) => {
242
+ resolve({ success: true, avatarUrl });
243
+ },
244
+ onError: (error) => {
245
+ resolve({ success: false, error });
246
+ },
247
+ });
248
+ });
249
+ }
250
+
251
+ /**
252
+ * Get wallet balance
253
+ */
254
+ async getWalletBalance(): Promise<number> {
255
+ return this.wallet.getBalance();
256
+ }
257
+
258
+ /**
259
+ * Get wallet transactions
260
+ */
261
+ async getWalletTransactions(page?: number): Promise<{
262
+ transactions: Transaction[];
263
+ next?: string;
264
+ previous?: string;
265
+ count: number;
266
+ }> {
267
+ return this.wallet.getTransactions(page);
268
+ }
269
+
270
+ /**
271
+ * Cleanup
272
+ */
273
+ destroy(): void {
274
+ this.stopAutoRefresh();
275
+ }
276
+ }
277
+
@@ -0,0 +1,223 @@
1
+ // src/lib/api/auth.ts
2
+ // ZO API authentication functions
3
+
4
+ import { ZoApiClient } from './client';
5
+ import { StorageAdapter, STORAGE_KEYS } from '../utils/storage';
6
+ import type {
7
+ ZoAuthOTPRequest,
8
+ ZoAuthOTPVerifyRequest,
9
+ ZoAuthResponse,
10
+ ZoErrorResponse,
11
+ } from '../types';
12
+
13
+ export class ZoAuth {
14
+ constructor(
15
+ private client: ZoApiClient,
16
+ private storage: StorageAdapter
17
+ ) {}
18
+
19
+ /**
20
+ * Send OTP to phone number
21
+ * Step 1 of ZO phone authentication
22
+ */
23
+ async sendOTP(
24
+ countryCode: string,
25
+ phoneNumber: string
26
+ ): Promise<{ success: boolean; message: string }> {
27
+ try {
28
+ const payload: ZoAuthOTPRequest = {
29
+ mobile_country_code: countryCode,
30
+ mobile_number: phoneNumber,
31
+ message_channel: '', // Empty string as per ZO API spec
32
+ };
33
+
34
+ const response = await this.client.axiosInstance.post(
35
+ '/api/v1/auth/login/mobile/otp/',
36
+ payload
37
+ );
38
+
39
+ if (response.status >= 200 && response.status < 300) {
40
+ return {
41
+ success: true,
42
+ message: response.data?.message || 'OTP sent successfully',
43
+ };
44
+ }
45
+
46
+ return {
47
+ success: false,
48
+ message: response.data?.message || `Unexpected status: ${response.status}`,
49
+ };
50
+ } catch (error: any) {
51
+ const errorData = error.response?.data as ZoErrorResponse;
52
+ const errorMessage = errorData?.detail || errorData?.message || errorData?.error || error.message || 'Failed to send OTP';
53
+
54
+ return {
55
+ success: false,
56
+ message: errorMessage,
57
+ };
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Verify OTP and authenticate user
63
+ * Step 2 of ZO phone authentication
64
+ * Returns full auth response with tokens and user profile
65
+ */
66
+ async verifyOTP(
67
+ countryCode: string,
68
+ phoneNumber: string,
69
+ otp: string
70
+ ): Promise<{
71
+ success: boolean;
72
+ data?: ZoAuthResponse;
73
+ error?: string;
74
+ }> {
75
+ try {
76
+ const payload: ZoAuthOTPVerifyRequest = {
77
+ mobile_country_code: countryCode,
78
+ mobile_number: phoneNumber,
79
+ otp,
80
+ };
81
+
82
+ const response = await this.client.axiosInstance.post<ZoAuthResponse>(
83
+ '/api/v1/auth/login/mobile/',
84
+ payload
85
+ );
86
+
87
+ // Parse response data if it's a string
88
+ let responseData: ZoAuthResponse;
89
+ if (typeof response.data === 'string') {
90
+ try {
91
+ responseData = JSON.parse(response.data);
92
+ } catch {
93
+ return {
94
+ success: false,
95
+ error: 'Invalid response format from authentication service',
96
+ };
97
+ }
98
+ } else {
99
+ responseData = response.data;
100
+ }
101
+
102
+ // Validate response structure
103
+ if (!responseData || !responseData.user || !responseData.access_token) {
104
+ return {
105
+ success: false,
106
+ error: 'Invalid response structure from authentication service',
107
+ };
108
+ }
109
+
110
+ // Store session data
111
+ await this.storeSession(responseData);
112
+
113
+ return {
114
+ success: true,
115
+ data: responseData,
116
+ };
117
+ } catch (error: any) {
118
+ const errorMessage = this.extractErrorMessage(error);
119
+ return {
120
+ success: false,
121
+ error: errorMessage,
122
+ };
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Refresh access token using refresh token
128
+ */
129
+ async refreshAccessToken(
130
+ refreshToken: string
131
+ ): Promise<{
132
+ success: boolean;
133
+ tokens?: {
134
+ access: string;
135
+ refresh: string;
136
+ access_expiry: string;
137
+ refresh_expiry: string;
138
+ };
139
+ error?: string;
140
+ }> {
141
+ try {
142
+ const response = await this.client.axiosInstance.post('/api/v1/auth/token/refresh/', {
143
+ refresh_token: refreshToken,
144
+ });
145
+
146
+ return {
147
+ success: true,
148
+ tokens: response.data,
149
+ };
150
+ } catch (error: any) {
151
+ return {
152
+ success: false,
153
+ error: 'Failed to refresh authentication',
154
+ };
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Check if user is authenticated
160
+ */
161
+ async checkLoginStatus(accessToken: string): Promise<{
162
+ success: boolean;
163
+ isAuthenticated: boolean;
164
+ }> {
165
+ try {
166
+ const response = await this.client.axiosInstance.get('/api/v1/auth/login/check/', {
167
+ headers: {
168
+ Authorization: `Bearer ${accessToken}`,
169
+ },
170
+ });
171
+
172
+ return {
173
+ success: true,
174
+ isAuthenticated: response.data.authenticated === true,
175
+ };
176
+ } catch {
177
+ return {
178
+ success: false,
179
+ isAuthenticated: false,
180
+ };
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Store session data in storage
186
+ */
187
+ private async storeSession(data: ZoAuthResponse): Promise<void> {
188
+ await this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, data.access_token);
189
+ await this.storage.setItem(STORAGE_KEYS.REFRESH_TOKEN, data.refresh_token);
190
+ await this.storage.setItem(STORAGE_KEYS.TOKEN_EXPIRY, data.access_token_expiry);
191
+ await this.storage.setItem(STORAGE_KEYS.REFRESH_EXPIRY, data.refresh_token_expiry);
192
+ await this.storage.setItem(STORAGE_KEYS.USER, JSON.stringify(data.user));
193
+
194
+ if (data.device_id) {
195
+ await this.storage.setItem(STORAGE_KEYS.CLIENT_DEVICE_ID, data.device_id);
196
+ }
197
+ if (data.device_secret) {
198
+ await this.storage.setItem(STORAGE_KEYS.CLIENT_DEVICE_SECRET, data.device_secret);
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Extract error message from various ZO API error formats
204
+ */
205
+ private extractErrorMessage(error: any): string {
206
+ const errorData = error.response?.data;
207
+
208
+ if (errorData) {
209
+ // Format 1: { success: false, errors: [...] }
210
+ if (errorData.errors && Array.isArray(errorData.errors)) {
211
+ return errorData.errors[0] || 'Invalid OTP';
212
+ }
213
+ // Format 2: { detail: "...", message: "..." }
214
+ if (errorData.detail) return errorData.detail;
215
+ if (errorData.message) return errorData.message;
216
+ // Format 3: { error: "..." }
217
+ if (errorData.error) return errorData.error;
218
+ }
219
+
220
+ return 'Authentication failed';
221
+ }
222
+ }
223
+