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
|
@@ -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
|
+
|