react-native-salespanda 0.4.3 → 0.4.5
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/lib/module/SalespandaApp.js +16 -8
- package/lib/module/SalespandaApp.js.map +1 -1
- package/lib/module/assets/images/index.js +20 -0
- package/lib/module/assets/images/index.js.map +1 -0
- package/lib/module/components/Loader.js +45 -0
- package/lib/module/components/Loader.js.map +1 -0
- package/lib/module/config/SalespandaConfig.js +17 -0
- package/lib/module/config/SalespandaConfig.js.map +1 -1
- package/lib/module/index.js +1 -32
- package/lib/module/index.js.map +1 -1
- package/lib/module/screens/Tabs/HomeScreen.js +174 -83
- package/lib/module/screens/Tabs/HomeScreen.js.map +1 -1
- package/lib/module/services/api.js +132 -0
- package/lib/module/services/api.js.map +1 -0
- package/lib/module/services/authService.js +59 -0
- package/lib/module/services/authService.js.map +1 -0
- package/lib/module/store/index.js +13 -0
- package/lib/module/store/index.js.map +1 -0
- package/lib/typescript/src/SalespandaApp.d.ts.map +1 -1
- package/lib/typescript/src/components/Loader.d.ts +11 -0
- package/lib/typescript/src/components/Loader.d.ts.map +1 -0
- package/lib/typescript/src/config/SalespandaConfig.d.ts +14 -0
- package/lib/typescript/src/config/SalespandaConfig.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -12
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/screens/Tabs/HomeScreen.d.ts.map +1 -1
- package/lib/typescript/src/services/api.d.ts +638 -0
- package/lib/typescript/src/services/api.d.ts.map +1 -0
- package/lib/typescript/src/services/authService.d.ts +13 -0
- package/lib/typescript/src/services/authService.d.ts.map +1 -0
- package/lib/typescript/src/store/index.d.ts +36 -0
- package/lib/typescript/src/store/index.d.ts.map +1 -0
- package/package.json +9 -3
- package/react-native.config.js +8 -5
- package/src/SalespandaApp.tsx +18 -10
- package/src/components/Loader.tsx +48 -0
- package/src/config/SalespandaConfig.ts +37 -0
- package/src/index.tsx +8 -30
- package/src/screens/Tabs/HomeScreen.tsx +220 -61
- package/src/services/api.ts +173 -0
- package/src/services/authService.ts +75 -0
- package/src/store/index.ts +16 -0
|
@@ -9,22 +9,42 @@ import {
|
|
|
9
9
|
FlatList,
|
|
10
10
|
Dimensions,
|
|
11
11
|
} from 'react-native';
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
scale,
|
|
14
|
+
verticalScale,
|
|
15
|
+
moderateScale,
|
|
16
|
+
moderateVerticalScale,
|
|
17
|
+
} from 'react-native-size-matters';
|
|
14
18
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
19
|
+
import Loader from '../../components/Loader';
|
|
15
20
|
import TabsHeader from '../../components/TabsHeader';
|
|
21
|
+
import { Colors } from '../../constants/Colors';
|
|
22
|
+
import {
|
|
23
|
+
useAuthenticateMutation,
|
|
24
|
+
useLazyGetHomeQuery,
|
|
25
|
+
useSpssoLoginMutation,
|
|
26
|
+
loadPersistedTokens,
|
|
27
|
+
persistTokens,
|
|
28
|
+
setRuntimeTokens,
|
|
29
|
+
clearPersistedTokens,
|
|
30
|
+
} from '../../services/api';
|
|
31
|
+
import type { HomeResponse } from '../../services/api';
|
|
16
32
|
|
|
17
33
|
interface MenuItemProps {
|
|
18
34
|
title: string;
|
|
19
|
-
|
|
35
|
+
imageUrl?: string | null;
|
|
20
36
|
onPress?: () => void;
|
|
21
37
|
}
|
|
22
38
|
|
|
23
|
-
const MenuItem: React.FC<MenuItemProps> = ({ title,
|
|
39
|
+
const MenuItem: React.FC<MenuItemProps> = ({ title, imageUrl, onPress }) => {
|
|
24
40
|
return (
|
|
25
41
|
<TouchableOpacity style={styles.menuItem} onPress={onPress}>
|
|
26
42
|
<View style={styles.iconContainer}>
|
|
27
|
-
|
|
43
|
+
{imageUrl ? (
|
|
44
|
+
<Image source={{ uri: imageUrl }} style={styles.menuItemImage} />
|
|
45
|
+
) : (
|
|
46
|
+
<Text style={styles.iconText}>📌</Text>
|
|
47
|
+
)}
|
|
28
48
|
</View>
|
|
29
49
|
<Text style={styles.menuItemText}>{title}</Text>
|
|
30
50
|
</TouchableOpacity>
|
|
@@ -32,14 +52,34 @@ const MenuItem: React.FC<MenuItemProps> = ({ title, icon, onPress }) => {
|
|
|
32
52
|
};
|
|
33
53
|
|
|
34
54
|
const HomeScreen: React.FC = () => {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
'
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
const [loading, setLoading] = React.useState<boolean>(false);
|
|
56
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
57
|
+
const [homeData, setHomeData] = React.useState<
|
|
58
|
+
HomeResponse['response'] | null
|
|
59
|
+
>(null);
|
|
60
|
+
|
|
61
|
+
type BannerItem = {
|
|
62
|
+
image: string;
|
|
63
|
+
title?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const [authenticate] = useAuthenticateMutation();
|
|
67
|
+
const [spssoLogin] = useSpssoLoginMutation();
|
|
68
|
+
const [triggerHome, { isFetching: isHomeFetching }] = useLazyGetHomeQuery();
|
|
69
|
+
|
|
70
|
+
const bannerItems: BannerItem[] = React.useMemo(
|
|
71
|
+
() =>
|
|
72
|
+
homeData?.home_banner && homeData.home_banner.length > 0
|
|
73
|
+
? homeData.home_banner.map((item) => ({
|
|
74
|
+
image: item.image,
|
|
75
|
+
title: item.title ?? undefined,
|
|
76
|
+
}))
|
|
77
|
+
: [],
|
|
78
|
+
[homeData?.home_banner]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const menuItems = React.useMemo(() => homeData?.menu ?? [], [homeData?.menu]);
|
|
82
|
+
|
|
43
83
|
const screenWidth = Dimensions.get('window').width;
|
|
44
84
|
const [activeIndex, setActiveIndex] = React.useState(0);
|
|
45
85
|
const flatListRef = React.useRef<any>(null);
|
|
@@ -49,10 +89,92 @@ const HomeScreen: React.FC = () => {
|
|
|
49
89
|
const index = Math.round(offsetX / screenWidth);
|
|
50
90
|
setActiveIndex(index);
|
|
51
91
|
};
|
|
92
|
+
|
|
93
|
+
const loadHomeData = React.useCallback(
|
|
94
|
+
async (isRetry?: boolean) => {
|
|
95
|
+
try {
|
|
96
|
+
setLoading(true);
|
|
97
|
+
setError(null);
|
|
98
|
+
|
|
99
|
+
const storedTokens = await loadPersistedTokens();
|
|
100
|
+
let accessToken = storedTokens.accessToken;
|
|
101
|
+
let token = storedTokens.token;
|
|
102
|
+
|
|
103
|
+
if (!accessToken) {
|
|
104
|
+
const authResp = await authenticate({}).unwrap();
|
|
105
|
+
accessToken = authResp.access_token;
|
|
106
|
+
setRuntimeTokens({ accessToken });
|
|
107
|
+
await persistTokens({ accessToken });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!token && accessToken) {
|
|
111
|
+
const ssoResp = await spssoLogin({
|
|
112
|
+
access_token: accessToken,
|
|
113
|
+
}).unwrap();
|
|
114
|
+
token = ssoResp.token;
|
|
115
|
+
setRuntimeTokens({ token });
|
|
116
|
+
await persistTokens({ token });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!token) {
|
|
120
|
+
throw new Error('Missing auth token, please try again.');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const tokenHeader: string | undefined = token ?? undefined;
|
|
124
|
+
const homeResp = await triggerHome({
|
|
125
|
+
tokenOverride: tokenHeader,
|
|
126
|
+
}).unwrap();
|
|
127
|
+
|
|
128
|
+
const isSuccess =
|
|
129
|
+
homeResp.statusCode === '200' &&
|
|
130
|
+
homeResp.status?.toLowerCase() === 'success';
|
|
131
|
+
if (!isSuccess) {
|
|
132
|
+
const code = homeResp.statusCode;
|
|
133
|
+
|
|
134
|
+
// If unauthorized, clear tokens once and retry
|
|
135
|
+
if (
|
|
136
|
+
!isRetry &&
|
|
137
|
+
code &&
|
|
138
|
+
(code === '1008' || code === '401' || code === '403')
|
|
139
|
+
) {
|
|
140
|
+
await clearPersistedTokens();
|
|
141
|
+
setRuntimeTokens({ token: null, accessToken: null });
|
|
142
|
+
await loadHomeData(true);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
throw new Error(homeResp.message || 'Failed to load home data');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
setHomeData(homeResp.response);
|
|
150
|
+
setLoading(false);
|
|
151
|
+
} catch (e: any) {
|
|
152
|
+
const statusCode = e?.status || e?.data?.statusCode;
|
|
153
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
154
|
+
// For unauthorized keep loader spinning (no static fallback)
|
|
155
|
+
setError(null);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
setError(
|
|
159
|
+
e?.data?.message ||
|
|
160
|
+
e?.message ||
|
|
161
|
+
'Failed to load home data. Please try again.'
|
|
162
|
+
);
|
|
163
|
+
setLoading(false);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
[authenticate, spssoLogin, triggerHome]
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
React.useEffect(() => {
|
|
170
|
+
loadHomeData();
|
|
171
|
+
}, [loadHomeData]);
|
|
172
|
+
|
|
52
173
|
React.useEffect(() => {
|
|
53
174
|
const id = setInterval(() => {
|
|
54
175
|
setActiveIndex((prev) => {
|
|
55
|
-
const next =
|
|
176
|
+
const next =
|
|
177
|
+
bannerItems.length > 0 ? (prev + 1) % bannerItems.length : 0;
|
|
56
178
|
if (flatListRef.current) {
|
|
57
179
|
try {
|
|
58
180
|
flatListRef.current.scrollToIndex({ index: next, animated: true });
|
|
@@ -63,23 +185,29 @@ const HomeScreen: React.FC = () => {
|
|
|
63
185
|
return next;
|
|
64
186
|
});
|
|
65
187
|
}, 2500);
|
|
66
|
-
return () =>
|
|
67
|
-
|
|
188
|
+
return () => {
|
|
189
|
+
clearInterval(id);
|
|
190
|
+
};
|
|
191
|
+
}, [bannerItems.length]);
|
|
68
192
|
|
|
69
193
|
return (
|
|
70
194
|
<SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}>
|
|
71
195
|
<View style={styles.container}>
|
|
72
196
|
<TabsHeader title="Home" />
|
|
73
|
-
|
|
74
|
-
|
|
197
|
+
|
|
198
|
+
<View style={styles.contentWrapper}>
|
|
199
|
+
{loading || isHomeFetching || !homeData ? (
|
|
200
|
+
<Loader overlay message="Loading home data..." />
|
|
201
|
+
) : null}
|
|
202
|
+
|
|
75
203
|
<View style={styles.carouselContainer}>
|
|
76
204
|
<FlatList
|
|
77
205
|
ref={flatListRef}
|
|
78
|
-
data={
|
|
79
|
-
keyExtractor={(
|
|
206
|
+
data={bannerItems}
|
|
207
|
+
keyExtractor={(item, idx) => `${item.title || 'banner'}-${idx}`}
|
|
80
208
|
renderItem={({ item }) => (
|
|
81
209
|
<Image
|
|
82
|
-
source={{ uri: item }}
|
|
210
|
+
source={{ uri: item.image }}
|
|
83
211
|
style={[styles.carouselImage, { width: screenWidth }]}
|
|
84
212
|
/>
|
|
85
213
|
)}
|
|
@@ -93,9 +221,8 @@ const HomeScreen: React.FC = () => {
|
|
|
93
221
|
index,
|
|
94
222
|
})}
|
|
95
223
|
/>
|
|
96
|
-
{/* Overlay Indicators at bottom of the image */}
|
|
97
224
|
<View style={styles.carouselIndicatorsOverlay}>
|
|
98
|
-
{
|
|
225
|
+
{bannerItems.map((_, idx) => (
|
|
99
226
|
<View
|
|
100
227
|
key={idx}
|
|
101
228
|
style={[
|
|
@@ -107,30 +234,32 @@ const HomeScreen: React.FC = () => {
|
|
|
107
234
|
</View>
|
|
108
235
|
</View>
|
|
109
236
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
237
|
+
<ScrollView
|
|
238
|
+
style={styles.scrollView}
|
|
239
|
+
contentContainerStyle={styles.scrollContent}
|
|
240
|
+
>
|
|
241
|
+
{error ? (
|
|
242
|
+
<>
|
|
243
|
+
<Text style={styles.errorText}>{error}</Text>
|
|
244
|
+
<TouchableOpacity onPress={() => loadHomeData()}>
|
|
245
|
+
<Text style={styles.retryText}>Tap to retry</Text>
|
|
246
|
+
</TouchableOpacity>
|
|
247
|
+
</>
|
|
248
|
+
) : null}
|
|
115
249
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
<MenuItem title="Proposal" icon="📋" />
|
|
130
|
-
|
|
131
|
-
<MenuItem title="Courses" icon="🎓" />
|
|
132
|
-
</View>
|
|
133
|
-
</ScrollView>
|
|
250
|
+
{menuItems.length > 0 ? (
|
|
251
|
+
<View style={styles.menuGrid}>
|
|
252
|
+
{menuItems.map((item) => (
|
|
253
|
+
<MenuItem
|
|
254
|
+
key={item.title}
|
|
255
|
+
title={item.title}
|
|
256
|
+
imageUrl={item.image}
|
|
257
|
+
/>
|
|
258
|
+
))}
|
|
259
|
+
</View>
|
|
260
|
+
) : null}
|
|
261
|
+
</ScrollView>
|
|
262
|
+
</View>
|
|
134
263
|
</View>
|
|
135
264
|
</SafeAreaView>
|
|
136
265
|
);
|
|
@@ -151,20 +280,24 @@ const styles = StyleSheet.create({
|
|
|
151
280
|
flex: 1,
|
|
152
281
|
backgroundColor: Colors.white,
|
|
153
282
|
},
|
|
283
|
+
scrollContent: {
|
|
284
|
+
paddingBottom: verticalScale(24),
|
|
285
|
+
},
|
|
154
286
|
carouselContainer: {
|
|
155
287
|
width: '100%',
|
|
156
|
-
backgroundColor: Colors.white,
|
|
288
|
+
backgroundColor: Colors.white,
|
|
157
289
|
position: 'relative',
|
|
158
290
|
},
|
|
159
291
|
carouselImage: {
|
|
160
|
-
height: verticalScale(
|
|
161
|
-
resizeMode: '
|
|
292
|
+
height: verticalScale(155),
|
|
293
|
+
resizeMode: 'contain',
|
|
294
|
+
backgroundColor: Colors.white,
|
|
162
295
|
},
|
|
163
296
|
carouselIndicatorsOverlay: {
|
|
164
297
|
position: 'absolute',
|
|
165
298
|
left: 0,
|
|
166
299
|
right: 0,
|
|
167
|
-
bottom: verticalScale(
|
|
300
|
+
bottom: verticalScale(20),
|
|
168
301
|
flexDirection: 'row',
|
|
169
302
|
justifyContent: 'center',
|
|
170
303
|
alignItems: 'center',
|
|
@@ -183,30 +316,28 @@ const styles = StyleSheet.create({
|
|
|
183
316
|
menuGrid: {
|
|
184
317
|
flexDirection: 'row',
|
|
185
318
|
flexWrap: 'wrap',
|
|
186
|
-
|
|
187
|
-
|
|
319
|
+
paddingTop: scale(8),
|
|
320
|
+
paddingBottom: verticalScale(16),
|
|
321
|
+
justifyContent: 'flex-start',
|
|
322
|
+
alignSelf: 'center',
|
|
323
|
+
width: '94%',
|
|
188
324
|
},
|
|
189
325
|
menuItem: {
|
|
190
|
-
width: '
|
|
326
|
+
width: '29.5%',
|
|
191
327
|
aspectRatio: 1,
|
|
192
328
|
backgroundColor: Colors.lightblue,
|
|
193
329
|
borderRadius: moderateScale(16),
|
|
194
|
-
padding:
|
|
195
|
-
marginBottom:
|
|
330
|
+
padding: moderateScale(12),
|
|
331
|
+
marginBottom: moderateVerticalScale(8),
|
|
332
|
+
marginHorizontal: scale(6),
|
|
196
333
|
alignItems: 'center',
|
|
197
334
|
justifyContent: 'center',
|
|
198
|
-
shadowColor: Colors.black,
|
|
199
|
-
shadowOffset: { width: 0, height: 0 },
|
|
200
|
-
shadowOpacity: 0.1,
|
|
201
|
-
shadowRadius: 0.5,
|
|
202
335
|
borderWidth: 1,
|
|
203
336
|
borderColor: Colors.border,
|
|
204
337
|
},
|
|
205
338
|
iconContainer: {
|
|
206
339
|
width: scale(50),
|
|
207
340
|
height: scale(50),
|
|
208
|
-
borderRadius: moderateScale(25),
|
|
209
|
-
backgroundColor: Colors.divider,
|
|
210
341
|
justifyContent: 'center',
|
|
211
342
|
alignItems: 'center',
|
|
212
343
|
marginBottom: verticalScale(8),
|
|
@@ -214,10 +345,38 @@ const styles = StyleSheet.create({
|
|
|
214
345
|
iconText: {
|
|
215
346
|
fontSize: moderateScale(24),
|
|
216
347
|
},
|
|
348
|
+
menuItemImage: {
|
|
349
|
+
width: scale(40),
|
|
350
|
+
height: scale(40),
|
|
351
|
+
borderRadius: moderateScale(20),
|
|
352
|
+
resizeMode: 'cover',
|
|
353
|
+
},
|
|
217
354
|
menuItemText: {
|
|
218
355
|
fontSize: moderateScale(11),
|
|
219
356
|
textAlign: 'center',
|
|
220
357
|
color: Colors.black,
|
|
221
358
|
fontWeight: '500',
|
|
222
359
|
},
|
|
360
|
+
statusText: {
|
|
361
|
+
fontSize: moderateScale(12),
|
|
362
|
+
color: Colors.black,
|
|
363
|
+
paddingHorizontal: scale(12),
|
|
364
|
+
paddingTop: verticalScale(8),
|
|
365
|
+
},
|
|
366
|
+
errorText: {
|
|
367
|
+
fontSize: moderateScale(12),
|
|
368
|
+
color: 'red',
|
|
369
|
+
paddingHorizontal: scale(12),
|
|
370
|
+
paddingTop: verticalScale(4),
|
|
371
|
+
},
|
|
372
|
+
retryText: {
|
|
373
|
+
fontSize: moderateScale(12),
|
|
374
|
+
color: Colors.primary,
|
|
375
|
+
paddingHorizontal: scale(12),
|
|
376
|
+
paddingTop: verticalScale(6),
|
|
377
|
+
},
|
|
378
|
+
contentWrapper: {
|
|
379
|
+
flex: 1,
|
|
380
|
+
position: 'relative',
|
|
381
|
+
},
|
|
223
382
|
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
|
3
|
+
|
|
4
|
+
const BASE_URL = 'https://app.sptestmfp.com/';
|
|
5
|
+
const DEFAULT_CLIENT_ID = 'vPUz1fFucjlNAV8rujFg7AfGbS3Ay/4=';
|
|
6
|
+
const DEFAULT_AGENT_ID = '3pYw3fp0iTwOge6wjf4i+Fm7QBbx';
|
|
7
|
+
const TOKENS_KEY = '@salespanda/sdk/tokens';
|
|
8
|
+
|
|
9
|
+
export type RuntimeTokens = {
|
|
10
|
+
token: string | null;
|
|
11
|
+
accessToken: string | null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type AuthenticateResponse = {
|
|
15
|
+
statusCode: string;
|
|
16
|
+
status: string;
|
|
17
|
+
message: string;
|
|
18
|
+
expire_datetime?: string;
|
|
19
|
+
access_token: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type SsoLoginResponse = {
|
|
23
|
+
statusCode: string;
|
|
24
|
+
status: string;
|
|
25
|
+
message: string;
|
|
26
|
+
token: string;
|
|
27
|
+
response?: unknown;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type HomeMenuItem = {
|
|
31
|
+
title: string;
|
|
32
|
+
desciption?: string | null;
|
|
33
|
+
image?: string | null;
|
|
34
|
+
banner_image?: string | null;
|
|
35
|
+
page_key?: string | null;
|
|
36
|
+
link?: string | null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type HomeBannerItem = {
|
|
40
|
+
title?: string | null;
|
|
41
|
+
desciption?: string | null;
|
|
42
|
+
image: string;
|
|
43
|
+
page_key?: string | null;
|
|
44
|
+
link?: string | null;
|
|
45
|
+
user_type?: string | null;
|
|
46
|
+
app_page?: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type HomeResponse = {
|
|
50
|
+
statusCode: string;
|
|
51
|
+
status: string;
|
|
52
|
+
message: string;
|
|
53
|
+
response: {
|
|
54
|
+
menu: HomeMenuItem[];
|
|
55
|
+
home_banner: HomeBannerItem[];
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
let runtimeTokens: RuntimeTokens = {
|
|
61
|
+
token: null,
|
|
62
|
+
accessToken: null,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const setRuntimeTokens = (tokens: Partial<RuntimeTokens>) => {
|
|
66
|
+
runtimeTokens = { ...runtimeTokens, ...tokens };
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const getRuntimeTokens = (): RuntimeTokens => runtimeTokens;
|
|
70
|
+
|
|
71
|
+
export const loadPersistedTokens = async (): Promise<RuntimeTokens> => {
|
|
72
|
+
try {
|
|
73
|
+
const stored = await AsyncStorage.getItem(TOKENS_KEY);
|
|
74
|
+
if (stored) {
|
|
75
|
+
const parsed = JSON.parse(stored) as RuntimeTokens;
|
|
76
|
+
runtimeTokens = {
|
|
77
|
+
token: parsed.token || null,
|
|
78
|
+
accessToken: parsed.accessToken || null,
|
|
79
|
+
};
|
|
80
|
+
return runtimeTokens;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// ignore storage failures
|
|
84
|
+
}
|
|
85
|
+
return { token: null, accessToken: null };
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const persistTokens = async (tokens: Partial<RuntimeTokens>) => {
|
|
89
|
+
const next = { ...runtimeTokens, ...tokens };
|
|
90
|
+
runtimeTokens = next;
|
|
91
|
+
try {
|
|
92
|
+
await AsyncStorage.setItem(TOKENS_KEY, JSON.stringify(next));
|
|
93
|
+
} catch {
|
|
94
|
+
// ignore storage failures
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const clearPersistedTokens = async () => {
|
|
99
|
+
runtimeTokens = { token: null, accessToken: null };
|
|
100
|
+
try {
|
|
101
|
+
await AsyncStorage.removeItem(TOKENS_KEY);
|
|
102
|
+
} catch {
|
|
103
|
+
// ignore storage failures
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const baseQuery = fetchBaseQuery({
|
|
108
|
+
baseUrl: BASE_URL,
|
|
109
|
+
prepareHeaders: (headers) => {
|
|
110
|
+
if (runtimeTokens.token) {
|
|
111
|
+
headers.set('Token', runtimeTokens.token);
|
|
112
|
+
}
|
|
113
|
+
return headers;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
export const salespandaApi = createApi({
|
|
118
|
+
reducerPath: 'salespandaApi',
|
|
119
|
+
baseQuery,
|
|
120
|
+
endpoints: (builder) => ({
|
|
121
|
+
authenticate: builder.mutation<
|
|
122
|
+
AuthenticateResponse,
|
|
123
|
+
{ agent_id?: string; clientId?: string }
|
|
124
|
+
>({
|
|
125
|
+
query: ({ agent_id = DEFAULT_AGENT_ID, clientId }) => ({
|
|
126
|
+
url: 'framework/api/authenticate',
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: {
|
|
129
|
+
'Content-Type': 'application/json',
|
|
130
|
+
'Client-Id': clientId || DEFAULT_CLIENT_ID,
|
|
131
|
+
},
|
|
132
|
+
body: { agent_id },
|
|
133
|
+
}),
|
|
134
|
+
}),
|
|
135
|
+
spssoLogin: builder.mutation<
|
|
136
|
+
SsoLoginResponse,
|
|
137
|
+
{ access_token: string; app_version?: string }
|
|
138
|
+
>({
|
|
139
|
+
query: ({ access_token, app_version = '1.0' }) => ({
|
|
140
|
+
url: 'framework/api/spssologin',
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: {
|
|
143
|
+
'Content-Type': 'application/json',
|
|
144
|
+
},
|
|
145
|
+
body: { app_version, access_token },
|
|
146
|
+
}),
|
|
147
|
+
}),
|
|
148
|
+
getHome: builder.query<
|
|
149
|
+
HomeResponse,
|
|
150
|
+
{ app_version?: string; tokenOverride?: string }
|
|
151
|
+
>({
|
|
152
|
+
query: ({ app_version = '5.0.1', tokenOverride } = {}) => ({
|
|
153
|
+
url: 'manager/apis/V2/app-home-screen.php',
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers: tokenOverride
|
|
156
|
+
? {
|
|
157
|
+
'Token': tokenOverride,
|
|
158
|
+
'Content-Type': 'application/json',
|
|
159
|
+
}
|
|
160
|
+
: {
|
|
161
|
+
'Content-Type': 'application/json',
|
|
162
|
+
},
|
|
163
|
+
body: { app_version },
|
|
164
|
+
}),
|
|
165
|
+
}),
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
export const {
|
|
170
|
+
useAuthenticateMutation,
|
|
171
|
+
useSpssoLoginMutation,
|
|
172
|
+
useLazyGetHomeQuery,
|
|
173
|
+
} = salespandaApi;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
import {
|
|
3
|
+
clearPersistedTokens,
|
|
4
|
+
getRuntimeTokens,
|
|
5
|
+
setRuntimeTokens,
|
|
6
|
+
} from './api';
|
|
7
|
+
|
|
8
|
+
const USER_KEY = '@salespanda/sdk/user';
|
|
9
|
+
|
|
10
|
+
export type AuthUser = {
|
|
11
|
+
id?: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
email?: string;
|
|
14
|
+
phone?: string;
|
|
15
|
+
raw?: unknown;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let runtimeUser: AuthUser | null = null;
|
|
19
|
+
|
|
20
|
+
export const login = async (
|
|
21
|
+
user?: AuthUser,
|
|
22
|
+
token?: string,
|
|
23
|
+
accessToken?: string
|
|
24
|
+
) => {
|
|
25
|
+
if (token) {
|
|
26
|
+
setRuntimeTokens({ token });
|
|
27
|
+
}
|
|
28
|
+
if (accessToken) {
|
|
29
|
+
setRuntimeTokens({ accessToken });
|
|
30
|
+
}
|
|
31
|
+
if (user) {
|
|
32
|
+
runtimeUser = user;
|
|
33
|
+
try {
|
|
34
|
+
await AsyncStorage.setItem(USER_KEY, JSON.stringify(user));
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore storage failures
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const logout = async () => {
|
|
42
|
+
runtimeUser = null;
|
|
43
|
+
await clearPersistedTokens();
|
|
44
|
+
try {
|
|
45
|
+
await AsyncStorage.removeItem(USER_KEY);
|
|
46
|
+
} catch {
|
|
47
|
+
// ignore storage failures
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const isAuthenticated = (): boolean => {
|
|
52
|
+
const tokens = getRuntimeTokens();
|
|
53
|
+
return Boolean(tokens.token || tokens.accessToken);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const getCurrentUser = async (): Promise<AuthUser | null> => {
|
|
57
|
+
if (runtimeUser) {
|
|
58
|
+
return runtimeUser;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const stored = await AsyncStorage.getItem(USER_KEY);
|
|
62
|
+
if (stored) {
|
|
63
|
+
runtimeUser = JSON.parse(stored) as AuthUser;
|
|
64
|
+
return runtimeUser;
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// ignore storage failures
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const getCurrentToken = (): string | null => {
|
|
73
|
+
const tokens = getRuntimeTokens();
|
|
74
|
+
return tokens.token || tokens.accessToken;
|
|
75
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { configureStore } from '@reduxjs/toolkit';
|
|
2
|
+
import { setupListeners } from '@reduxjs/toolkit/query';
|
|
3
|
+
import { salespandaApi } from '../services/api';
|
|
4
|
+
|
|
5
|
+
export const store = configureStore({
|
|
6
|
+
reducer: {
|
|
7
|
+
[salespandaApi.reducerPath]: salespandaApi.reducer,
|
|
8
|
+
},
|
|
9
|
+
middleware: (getDefaultMiddleware) =>
|
|
10
|
+
getDefaultMiddleware().concat(salespandaApi.middleware),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
setupListeners(store.dispatch);
|
|
14
|
+
|
|
15
|
+
export type RootState = ReturnType<typeof store.getState>;
|
|
16
|
+
export type AppDispatch = typeof store.dispatch;
|