react-native-salespanda 0.4.4 → 0.5.3

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 (45) hide show
  1. package/lib/module/SalespandaApp.js +16 -8
  2. package/lib/module/SalespandaApp.js.map +1 -1
  3. package/lib/module/assets/images/index.js +1 -22
  4. package/lib/module/assets/images/index.js.map +1 -1
  5. package/lib/module/components/Loader.js +45 -0
  6. package/lib/module/components/Loader.js.map +1 -0
  7. package/lib/module/config/SalespandaConfig.js +17 -0
  8. package/lib/module/config/SalespandaConfig.js.map +1 -1
  9. package/lib/module/index.js +1 -32
  10. package/lib/module/index.js.map +1 -1
  11. package/lib/module/navigation/BottomTabNavigator.js +1 -3
  12. package/lib/module/navigation/BottomTabNavigator.js.map +1 -1
  13. package/lib/module/screens/Tabs/HomeScreen.js +184 -104
  14. package/lib/module/screens/Tabs/HomeScreen.js.map +1 -1
  15. package/lib/module/services/api.js +132 -0
  16. package/lib/module/services/api.js.map +1 -0
  17. package/lib/module/services/authService.js +59 -0
  18. package/lib/module/services/authService.js.map +1 -0
  19. package/lib/module/store/index.js +13 -0
  20. package/lib/module/store/index.js.map +1 -0
  21. package/lib/typescript/src/SalespandaApp.d.ts.map +1 -1
  22. package/lib/typescript/src/components/Loader.d.ts +11 -0
  23. package/lib/typescript/src/components/Loader.d.ts.map +1 -0
  24. package/lib/typescript/src/config/SalespandaConfig.d.ts +14 -0
  25. package/lib/typescript/src/config/SalespandaConfig.d.ts.map +1 -1
  26. package/lib/typescript/src/index.d.ts +1 -12
  27. package/lib/typescript/src/index.d.ts.map +1 -1
  28. package/lib/typescript/src/screens/Tabs/HomeScreen.d.ts.map +1 -1
  29. package/lib/typescript/src/services/api.d.ts +638 -0
  30. package/lib/typescript/src/services/api.d.ts.map +1 -0
  31. package/lib/typescript/src/services/authService.d.ts +13 -0
  32. package/lib/typescript/src/services/authService.d.ts.map +1 -0
  33. package/lib/typescript/src/store/index.d.ts +36 -0
  34. package/lib/typescript/src/store/index.d.ts.map +1 -0
  35. package/package.json +8 -2
  36. package/react-native.config.js +8 -5
  37. package/src/SalespandaApp.tsx +18 -10
  38. package/src/components/Loader.tsx +48 -0
  39. package/src/config/SalespandaConfig.ts +37 -0
  40. package/src/index.tsx +8 -30
  41. package/src/navigation/BottomTabNavigator.tsx +1 -3
  42. package/src/screens/Tabs/HomeScreen.tsx +229 -73
  43. package/src/services/api.ts +173 -0
  44. package/src/services/authService.ts +75 -0
  45. package/src/store/index.ts +16 -0
@@ -9,37 +9,80 @@ import {
9
9
  FlatList,
10
10
  Dimensions,
11
11
  } from 'react-native';
12
- import { scale, verticalScale, moderateScale } from 'react-native-size-matters';
13
- import { Colors } from '../../constants/Colors';
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
- icon: string;
35
+ imageUrl?: string | null;
20
36
  onPress?: () => void;
21
37
  }
22
38
 
23
- const MenuItem: React.FC<MenuItemProps> = ({ title, icon, onPress }) => {
39
+ const MenuItem: React.FC<MenuItemProps> = ({ imageUrl, onPress }) => {
24
40
  return (
25
- <TouchableOpacity style={styles.menuItem} onPress={onPress}>
26
- <View style={styles.iconContainer}>
27
- <Text style={styles.iconText}>{icon}</Text>
28
- </View>
29
- <Text style={styles.menuItemText}>{title}</Text>
41
+ <TouchableOpacity
42
+ style={styles.menuItem}
43
+ onPress={onPress}
44
+ activeOpacity={0.8}
45
+ >
46
+ {imageUrl ? (
47
+ <Image source={{ uri: imageUrl }} style={styles.menuItemImage} />
48
+ ) : (
49
+ <Loader message="" />
50
+ )}
30
51
  </TouchableOpacity>
31
52
  );
32
53
  };
33
54
 
34
55
  const HomeScreen: React.FC = () => {
35
- const images = [
36
- 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?q=80&w=1200&auto=format&fit=crop',
37
- 'https://images.unsplash.com/photo-1482192596544-9eb780fc7f66?q=80&w=1200&auto=format&fit=crop',
38
- 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?q=80&w=1200&auto=format&fit=crop',
39
- 'https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1200&auto=format&fit=crop',
40
- 'https://images.unsplash.com/photo-1460353581641-37baddab0fa2?q=80&w=1200&auto=format&fit=crop',
41
- 'https://images.unsplash.com/photo-1496302662116-35cc4f36df92?q=80&w=1200&auto=format&fit=crop',
42
- ];
56
+ const [loading, setLoading] = React.useState<boolean>(false);
57
+ const [error, setError] = React.useState<string | null>(null);
58
+ const [homeData, setHomeData] = React.useState<
59
+ HomeResponse['response'] | null
60
+ >(null);
61
+
62
+ type BannerItem = {
63
+ image: string;
64
+ title?: string;
65
+ link?: string | null;
66
+ };
67
+
68
+ const [authenticate] = useAuthenticateMutation();
69
+ const [spssoLogin] = useSpssoLoginMutation();
70
+ const [triggerHome, { isFetching: isHomeFetching }] = useLazyGetHomeQuery();
71
+
72
+ const bannerItems: BannerItem[] = React.useMemo(
73
+ () =>
74
+ homeData?.home_banner && homeData.home_banner.length > 0
75
+ ? homeData.home_banner.map((item) => ({
76
+ image: item.image,
77
+ title: item.title ?? undefined,
78
+ link: item.link ?? null,
79
+ }))
80
+ : [],
81
+ [homeData?.home_banner]
82
+ );
83
+
84
+ const menuItems = React.useMemo(() => homeData?.menu ?? [], [homeData?.menu]);
85
+
43
86
  const screenWidth = Dimensions.get('window').width;
44
87
  const [activeIndex, setActiveIndex] = React.useState(0);
45
88
  const flatListRef = React.useRef<any>(null);
@@ -49,10 +92,90 @@ const HomeScreen: React.FC = () => {
49
92
  const index = Math.round(offsetX / screenWidth);
50
93
  setActiveIndex(index);
51
94
  };
95
+
96
+ const loadHomeData = React.useCallback(
97
+ async (isRetry?: boolean) => {
98
+ try {
99
+ setLoading(true);
100
+ setError(null);
101
+
102
+ const storedTokens = await loadPersistedTokens();
103
+ let accessToken = storedTokens.accessToken;
104
+ let token = storedTokens.token;
105
+
106
+ if (!accessToken) {
107
+ const authResp = await authenticate({}).unwrap();
108
+ accessToken = authResp.access_token;
109
+ setRuntimeTokens({ accessToken });
110
+ await persistTokens({ accessToken });
111
+ }
112
+
113
+ if (!token && accessToken) {
114
+ const ssoResp = await spssoLogin({
115
+ access_token: accessToken,
116
+ }).unwrap();
117
+ token = ssoResp.token;
118
+ setRuntimeTokens({ token });
119
+ await persistTokens({ token });
120
+ }
121
+
122
+ if (!token) {
123
+ throw new Error('Missing auth token, please try again.');
124
+ }
125
+
126
+ const tokenHeader: string | undefined = token ?? undefined;
127
+ const homeResp = await triggerHome({
128
+ tokenOverride: tokenHeader,
129
+ }).unwrap();
130
+
131
+ const isSuccess =
132
+ homeResp.statusCode === '200' &&
133
+ homeResp.status?.toLowerCase() === 'success';
134
+ if (!isSuccess) {
135
+ const code = homeResp.statusCode;
136
+
137
+ if (
138
+ !isRetry &&
139
+ code &&
140
+ (code === '1008' || code === '401' || code === '403')
141
+ ) {
142
+ await clearPersistedTokens();
143
+ setRuntimeTokens({ token: null, accessToken: null });
144
+ await loadHomeData(true);
145
+ return;
146
+ }
147
+
148
+ throw new Error(homeResp?.message || 'Failed to load home data');
149
+ }
150
+
151
+ setHomeData(homeResp.response);
152
+ setLoading(false);
153
+ } catch (e: any) {
154
+ const statusCode = e?.status || e?.data?.statusCode;
155
+ if (statusCode === 401 || statusCode === 403) {
156
+ setError(null);
157
+ return;
158
+ }
159
+ setError(
160
+ e?.data?.message ||
161
+ e?.message ||
162
+ 'Failed to load home data. Please try again.'
163
+ );
164
+ setLoading(false);
165
+ }
166
+ },
167
+ [authenticate, spssoLogin, triggerHome]
168
+ );
169
+
170
+ React.useEffect(() => {
171
+ loadHomeData();
172
+ }, [loadHomeData]);
173
+
52
174
  React.useEffect(() => {
53
175
  const id = setInterval(() => {
54
176
  setActiveIndex((prev) => {
55
- const next = (prev + 1) % images.length;
177
+ const next =
178
+ bannerItems.length > 0 ? (prev + 1) % bannerItems.length : 0;
56
179
  if (flatListRef.current) {
57
180
  try {
58
181
  flatListRef.current.scrollToIndex({ index: next, animated: true });
@@ -63,25 +186,33 @@ const HomeScreen: React.FC = () => {
63
186
  return next;
64
187
  });
65
188
  }, 2500);
66
- return () => clearInterval(id);
67
- }, [images.length]);
189
+ return () => {
190
+ clearInterval(id);
191
+ };
192
+ }, [bannerItems.length]);
68
193
 
69
194
  return (
70
195
  <SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}>
71
196
  <View style={styles.container}>
72
197
  <TabsHeader title="Home" />
73
- <ScrollView style={styles.scrollView}>
74
- {/* Edge-to-edge Image Carousel */}
198
+
199
+ <View style={styles.contentWrapper}>
200
+ {loading || isHomeFetching || !homeData ? (
201
+ <Loader overlay message="Loading home data..." />
202
+ ) : null}
203
+
75
204
  <View style={styles.carouselContainer}>
76
205
  <FlatList
77
206
  ref={flatListRef}
78
- data={images}
79
- keyExtractor={(_, idx) => `${idx}`}
207
+ data={bannerItems}
208
+ keyExtractor={(item, idx) => `${item.title || 'banner'}-${idx}`}
80
209
  renderItem={({ item }) => (
81
- <Image
82
- source={{ uri: item }}
83
- style={[styles.carouselImage, { width: screenWidth }]}
84
- />
210
+ <TouchableOpacity activeOpacity={0.9} onPress={() => {}}>
211
+ <Image
212
+ source={{ uri: item.image }}
213
+ style={[styles.carouselImage, { width: screenWidth }]}
214
+ />
215
+ </TouchableOpacity>
85
216
  )}
86
217
  horizontal
87
218
  pagingEnabled
@@ -93,9 +224,8 @@ const HomeScreen: React.FC = () => {
93
224
  index,
94
225
  })}
95
226
  />
96
- {/* Overlay Indicators at bottom of the image */}
97
227
  <View style={styles.carouselIndicatorsOverlay}>
98
- {images.map((_, idx) => (
228
+ {bannerItems.map((_, idx) => (
99
229
  <View
100
230
  key={idx}
101
231
  style={[
@@ -107,30 +237,32 @@ const HomeScreen: React.FC = () => {
107
237
  </View>
108
238
  </View>
109
239
 
110
- {/* Menu Grid */}
111
- <View style={styles.menuGrid}>
112
- <MenuItem title="Content Library" icon="📚" />
113
- <MenuItem title="DigiCard" icon="�" />
114
- <MenuItem title="Leads" icon="🎯" />
115
-
116
- <MenuItem title="Email Campaign" icon="📧" />
117
- <MenuItem title="Cadence" icon="⏰" />
118
- <MenuItem title="Social Setup" icon="�" />
119
-
120
- <MenuItem title="Quiz,Calculator & Referrals" icon="❓" />
121
- <MenuItem title="My Profile" icon="�" />
240
+ <ScrollView
241
+ style={styles.scrollView}
242
+ contentContainerStyle={styles.scrollContent}
243
+ >
244
+ {error ? (
245
+ <>
246
+ <Text style={styles.errorText}>{error}</Text>
247
+ <TouchableOpacity onPress={() => loadHomeData()}>
248
+ <Text style={styles.retryText}>Tap to retry</Text>
249
+ </TouchableOpacity>
250
+ </>
251
+ ) : null}
122
252
 
123
- <MenuItem title="Microsite Setup" icon="⚙️" />
124
- <MenuItem title="My Activity" icon="📊" />
125
- <MenuItem title="Internal Communication" icon="💬" />
126
-
127
- <MenuItem title="Help Videos" icon="🎥" />
128
- <MenuItem title="Import Gmail" icon="�" />
129
- <MenuItem title="Proposal" icon="📋" />
130
-
131
- <MenuItem title="Courses" icon="🎓" />
132
- </View>
133
- </ScrollView>
253
+ {menuItems.length > 0 ? (
254
+ <View style={styles.menuGrid}>
255
+ {menuItems.map((item) => (
256
+ <MenuItem
257
+ key={item.title}
258
+ title={item.title}
259
+ imageUrl={item.image}
260
+ />
261
+ ))}
262
+ </View>
263
+ ) : null}
264
+ </ScrollView>
265
+ </View>
134
266
  </View>
135
267
  </SafeAreaView>
136
268
  );
@@ -151,20 +283,24 @@ const styles = StyleSheet.create({
151
283
  flex: 1,
152
284
  backgroundColor: Colors.white,
153
285
  },
286
+ scrollContent: {
287
+ paddingBottom: verticalScale(24),
288
+ },
154
289
  carouselContainer: {
155
290
  width: '100%',
156
- backgroundColor: Colors.white, // subtle backplate for image load
291
+ backgroundColor: Colors.white,
157
292
  position: 'relative',
158
293
  },
159
294
  carouselImage: {
160
- height: verticalScale(200),
161
- resizeMode: 'cover',
295
+ height: verticalScale(155),
296
+ resizeMode: 'contain',
297
+ backgroundColor: Colors.white,
162
298
  },
163
299
  carouselIndicatorsOverlay: {
164
300
  position: 'absolute',
165
301
  left: 0,
166
302
  right: 0,
167
- bottom: verticalScale(12),
303
+ bottom: verticalScale(20),
168
304
  flexDirection: 'row',
169
305
  justifyContent: 'center',
170
306
  alignItems: 'center',
@@ -183,30 +319,23 @@ const styles = StyleSheet.create({
183
319
  menuGrid: {
184
320
  flexDirection: 'row',
185
321
  flexWrap: 'wrap',
186
- padding: scale(8),
187
- justifyContent: 'space-between',
322
+ paddingTop: scale(8),
323
+ paddingBottom: verticalScale(16),
324
+ justifyContent: 'flex-start',
325
+ alignSelf: 'center',
326
+ width: '100%',
188
327
  },
189
328
  menuItem: {
190
- width: '31%',
329
+ width: '29.5%',
191
330
  aspectRatio: 1,
192
- backgroundColor: Colors.lightblue,
193
331
  borderRadius: moderateScale(16),
194
- padding: scale(12),
195
- marginBottom: verticalScale(12),
196
- alignItems: 'center',
197
- justifyContent: 'center',
198
- shadowColor: Colors.black,
199
- shadowOffset: { width: 0, height: 0 },
200
- shadowOpacity: 0.1,
201
- shadowRadius: 0.5,
202
- borderWidth: 1,
203
- borderColor: Colors.border,
332
+ marginBottom: moderateVerticalScale(8),
333
+ marginHorizontal: scale(6),
334
+ overflow: 'hidden',
204
335
  },
205
336
  iconContainer: {
206
337
  width: scale(50),
207
338
  height: scale(50),
208
- borderRadius: moderateScale(25),
209
- backgroundColor: Colors.divider,
210
339
  justifyContent: 'center',
211
340
  alignItems: 'center',
212
341
  marginBottom: verticalScale(8),
@@ -214,10 +343,37 @@ const styles = StyleSheet.create({
214
343
  iconText: {
215
344
  fontSize: moderateScale(24),
216
345
  },
346
+ menuItemImage: {
347
+ width: '100%',
348
+ height: '100%',
349
+ resizeMode: 'cover',
350
+ },
217
351
  menuItemText: {
218
352
  fontSize: moderateScale(11),
219
353
  textAlign: 'center',
220
354
  color: Colors.black,
221
355
  fontWeight: '500',
222
356
  },
357
+ statusText: {
358
+ fontSize: moderateScale(12),
359
+ color: Colors.black,
360
+ paddingHorizontal: scale(12),
361
+ paddingTop: verticalScale(8),
362
+ },
363
+ errorText: {
364
+ fontSize: moderateScale(12),
365
+ color: 'red',
366
+ paddingHorizontal: scale(12),
367
+ paddingTop: verticalScale(4),
368
+ },
369
+ retryText: {
370
+ fontSize: moderateScale(12),
371
+ color: Colors.primary,
372
+ paddingHorizontal: scale(12),
373
+ paddingTop: verticalScale(6),
374
+ },
375
+ contentWrapper: {
376
+ flex: 1,
377
+ position: 'relative',
378
+ },
223
379
  });
@@ -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;