create-stackr 0.2.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 +21 -0
- package/README.md +642 -0
- package/bin/cli.js +12 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +113 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/dependencies.d.ts +82 -0
- package/dist/config/dependencies.d.ts.map +1 -0
- package/dist/config/dependencies.js +82 -0
- package/dist/config/dependencies.js.map +1 -0
- package/dist/config/presets.d.ts +3 -0
- package/dist/config/presets.d.ts.map +1 -0
- package/dist/config/presets.js +174 -0
- package/dist/config/presets.js.map +1 -0
- package/dist/generators/index.d.ts +40 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +130 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/onboarding.d.ts +8 -0
- package/dist/generators/onboarding.d.ts.map +1 -0
- package/dist/generators/onboarding.js +141 -0
- package/dist/generators/onboarding.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/features.d.ts +14 -0
- package/dist/prompts/features.d.ts.map +1 -0
- package/dist/prompts/features.js +96 -0
- package/dist/prompts/features.js.map +1 -0
- package/dist/prompts/index.d.ts +3 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +93 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/onboarding.d.ts +6 -0
- package/dist/prompts/onboarding.d.ts.map +1 -0
- package/dist/prompts/onboarding.js +37 -0
- package/dist/prompts/onboarding.js.map +1 -0
- package/dist/prompts/orm.d.ts +3 -0
- package/dist/prompts/orm.d.ts.map +1 -0
- package/dist/prompts/orm.js +23 -0
- package/dist/prompts/orm.js.map +1 -0
- package/dist/prompts/packageManager.d.ts +2 -0
- package/dist/prompts/packageManager.d.ts.map +1 -0
- package/dist/prompts/packageManager.js +18 -0
- package/dist/prompts/packageManager.js.map +1 -0
- package/dist/prompts/platform.d.ts +3 -0
- package/dist/prompts/platform.d.ts.map +1 -0
- package/dist/prompts/platform.js +21 -0
- package/dist/prompts/platform.js.map +1 -0
- package/dist/prompts/preset.d.ts +4 -0
- package/dist/prompts/preset.d.ts.map +1 -0
- package/dist/prompts/preset.js +165 -0
- package/dist/prompts/preset.js.map +1 -0
- package/dist/prompts/project.d.ts +2 -0
- package/dist/prompts/project.d.ts.map +1 -0
- package/dist/prompts/project.js +27 -0
- package/dist/prompts/project.js.map +1 -0
- package/dist/prompts/sdks.d.ts +2 -0
- package/dist/prompts/sdks.d.ts.map +1 -0
- package/dist/prompts/sdks.js +46 -0
- package/dist/prompts/sdks.js.map +1 -0
- package/dist/types/index.d.ts +77 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +25 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/cleanup.d.ts +5 -0
- package/dist/utils/cleanup.d.ts.map +1 -0
- package/dist/utils/cleanup.js +38 -0
- package/dist/utils/cleanup.js.map +1 -0
- package/dist/utils/copy.d.ts +10 -0
- package/dist/utils/copy.d.ts.map +1 -0
- package/dist/utils/copy.js +53 -0
- package/dist/utils/copy.js.map +1 -0
- package/dist/utils/errors.d.ts +33 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +136 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/git.d.ts +5 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +33 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +22 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/package.d.ts +16 -0
- package/dist/utils/package.d.ts.map +1 -0
- package/dist/utils/package.js +86 -0
- package/dist/utils/package.js.map +1 -0
- package/dist/utils/system-validation.d.ts +9 -0
- package/dist/utils/system-validation.d.ts.map +1 -0
- package/dist/utils/system-validation.js +31 -0
- package/dist/utils/system-validation.js.map +1 -0
- package/dist/utils/template.d.ts +20 -0
- package/dist/utils/template.d.ts.map +1 -0
- package/dist/utils/template.js +234 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/validation.d.ts +8 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +94 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +96 -0
- package/templates/base/backend/.dockerignore.ejs +62 -0
- package/templates/base/backend/.env.example.ejs +116 -0
- package/templates/base/backend/Dockerfile.ejs +142 -0
- package/templates/base/backend/controllers/event-queue/index.ts +20 -0
- package/templates/base/backend/controllers/event-queue/workers/user.ts +39 -0
- package/templates/base/backend/controllers/rest-api/index.ts +48 -0
- package/templates/base/backend/controllers/rest-api/plugins/auth.ts +152 -0
- package/templates/base/backend/controllers/rest-api/plugins/config.ts +64 -0
- package/templates/base/backend/controllers/rest-api/plugins/error-handler.ts +118 -0
- package/templates/base/backend/controllers/rest-api/routes/auth.ts.ejs +180 -0
- package/templates/base/backend/controllers/rest-api/routes/device-sessions.ts +197 -0
- package/templates/base/backend/controllers/rest-api/routes/oauth-web.ts.ejs +375 -0
- package/templates/base/backend/controllers/rest-api/server.ts.ejs +87 -0
- package/templates/base/backend/domain/device-session/repository.drizzle.ts +209 -0
- package/templates/base/backend/domain/device-session/repository.prisma.ts +248 -0
- package/templates/base/backend/domain/device-session/schema.ts +72 -0
- package/templates/base/backend/domain/session/repository.drizzle.ts +72 -0
- package/templates/base/backend/domain/session/repository.prisma.ts +72 -0
- package/templates/base/backend/domain/session/schema.ts +29 -0
- package/templates/base/backend/domain/user/repository.drizzle.ts +127 -0
- package/templates/base/backend/domain/user/repository.prisma.ts +115 -0
- package/templates/base/backend/domain/user/schema.ts +14 -0
- package/templates/base/backend/drizzle/schema.drizzle.ts +111 -0
- package/templates/base/backend/drizzle.config.drizzle.ts +13 -0
- package/templates/base/backend/lib/auth.drizzle.ts.ejs +104 -0
- package/templates/base/backend/lib/auth.prisma.ts.ejs +97 -0
- package/templates/base/backend/lib/constants.ts.ejs +29 -0
- package/templates/base/backend/package.json.ejs +50 -0
- package/templates/base/backend/prisma/schema.prisma.ejs +102 -0
- package/templates/base/backend/prisma.config.prisma.ts +12 -0
- package/templates/base/backend/tsconfig.json +39 -0
- package/templates/base/backend/utils/db.drizzle.ts +41 -0
- package/templates/base/backend/utils/db.prisma.ts +51 -0
- package/templates/base/backend/utils/email.ts.ejs +35 -0
- package/templates/base/backend/utils/errors.ts +348 -0
- package/templates/base/backend/utils/redis.ts.ejs +279 -0
- package/templates/base/mobile/.env.example.ejs +35 -0
- package/templates/base/mobile/.gitignore.ejs +167 -0
- package/templates/base/mobile/app/+not-found.tsx +85 -0
- package/templates/base/mobile/app/_layout.tsx.ejs +71 -0
- package/templates/base/mobile/app.json.ejs +88 -0
- package/templates/base/mobile/assets/images/adaptive-icon.png +0 -0
- package/templates/base/mobile/assets/images/favicon.png +0 -0
- package/templates/base/mobile/assets/images/icon.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_1.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_2.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_3.png +0 -0
- package/templates/base/mobile/assets/images/paywall_image.png +0 -0
- package/templates/base/mobile/assets/images/splash.png +0 -0
- package/templates/base/mobile/eas.json.ejs +49 -0
- package/templates/base/mobile/metro.config.js +9 -0
- package/templates/base/mobile/package.json.ejs +53 -0
- package/templates/base/mobile/src/components/ui/Button.tsx +131 -0
- package/templates/base/mobile/src/components/ui/Card.tsx +68 -0
- package/templates/base/mobile/src/components/ui/IconSymbol.tsx +90 -0
- package/templates/base/mobile/src/components/ui/Input.tsx +142 -0
- package/templates/base/mobile/src/components/ui/LoadingSpinner.tsx +98 -0
- package/templates/base/mobile/src/components/ui/OnboardingLayout.tsx +356 -0
- package/templates/base/mobile/src/components/ui/PaywallLayout.tsx +311 -0
- package/templates/base/mobile/src/components/ui/Skeleton.tsx +58 -0
- package/templates/base/mobile/src/components/ui/index.ts +6 -0
- package/templates/base/mobile/src/constants/Theme.ts +163 -0
- package/templates/base/mobile/src/context/ThemeContext.tsx +157 -0
- package/templates/base/mobile/src/lib/auth-client.ts.ejs +51 -0
- package/templates/base/mobile/src/services/api.ts.ejs +71 -0
- package/templates/base/mobile/src/services/errorService.ts +179 -0
- package/templates/base/mobile/src/services/sdkInitializer.ts.ejs +36 -0
- package/templates/base/mobile/src/store/index.ts.ejs +18 -0
- package/templates/base/mobile/src/store/ui.store.ts +100 -0
- package/templates/base/mobile/src/utils/formatters.ts +105 -0
- package/templates/base/mobile/src/utils/logger.ts +73 -0
- package/templates/base/mobile/src/utils/responsive.ts +234 -0
- package/templates/base/mobile/tsconfig.json +32 -0
- package/templates/base/web/.env.example.ejs +26 -0
- package/templates/base/web/components.json +22 -0
- package/templates/base/web/eslint.config.mjs +18 -0
- package/templates/base/web/next.config.ts +7 -0
- package/templates/base/web/package.json.ejs +35 -0
- package/templates/base/web/postcss.config.mjs +7 -0
- package/templates/base/web/public/.gitkeep +0 -0
- package/templates/base/web/public/file.svg +1 -0
- package/templates/base/web/public/globe.svg +1 -0
- package/templates/base/web/public/next.svg +1 -0
- package/templates/base/web/public/vercel.svg +1 -0
- package/templates/base/web/public/window.svg +1 -0
- package/templates/base/web/src/app/favicon.ico +0 -0
- package/templates/base/web/src/app/globals.css +152 -0
- package/templates/base/web/src/app/layout.tsx.ejs +54 -0
- package/templates/base/web/src/app/page.tsx.ejs +92 -0
- package/templates/base/web/src/components/auth/auth-hydrator.tsx.ejs +19 -0
- package/templates/base/web/src/components/auth/protected-route.tsx.ejs +109 -0
- package/templates/base/web/src/components/providers/device-session-setup.tsx.ejs +56 -0
- package/templates/base/web/src/components/providers/theme-provider.tsx +17 -0
- package/templates/base/web/src/components/theme-toggle.tsx +34 -0
- package/templates/base/web/src/components/ui/button.tsx +62 -0
- package/templates/base/web/src/components/ui/card.tsx +92 -0
- package/templates/base/web/src/components/ui/input.tsx +21 -0
- package/templates/base/web/src/components/ui/label.tsx +24 -0
- package/templates/base/web/src/components/ui/skeleton.tsx +13 -0
- package/templates/base/web/src/components/ui/spinner.tsx +20 -0
- package/templates/base/web/src/hooks/use-device-session.ts.ejs +40 -0
- package/templates/base/web/src/hooks/use-session.ts.ejs +56 -0
- package/templates/base/web/src/lib/auth/actions.ts.ejs +334 -0
- package/templates/base/web/src/lib/auth/config.ts.ejs +65 -0
- package/templates/base/web/src/lib/auth/cookies.ts.ejs +74 -0
- package/templates/base/web/src/lib/auth/index.ts.ejs +40 -0
- package/templates/base/web/src/lib/auth/oauth.ts.ejs +72 -0
- package/templates/base/web/src/lib/auth/pkce.ts.ejs +48 -0
- package/templates/base/web/src/lib/auth/sessions.ts.ejs +135 -0
- package/templates/base/web/src/lib/auth/user-agent.ts.ejs +47 -0
- package/templates/base/web/src/lib/device/actions.ts.ejs +148 -0
- package/templates/base/web/src/lib/device/id.ts.ejs +74 -0
- package/templates/base/web/src/lib/utils.ts +6 -0
- package/templates/base/web/src/proxy.ts.ejs +66 -0
- package/templates/base/web/src/store/auth.store.ts.ejs +89 -0
- package/templates/base/web/src/store/deviceSession.store.ts.ejs +141 -0
- package/templates/base/web/tsconfig.json +34 -0
- package/templates/features/mobile/auth/app/(auth)/_layout.tsx +16 -0
- package/templates/features/mobile/auth/app/(auth)/login.tsx +86 -0
- package/templates/features/mobile/auth/app/(auth)/register.tsx +86 -0
- package/templates/features/mobile/auth/components/auth/LoginForm.tsx.ejs +349 -0
- package/templates/features/mobile/auth/components/auth/RegisterForm.tsx.ejs +407 -0
- package/templates/features/mobile/auth/components/auth/index.ts +2 -0
- package/templates/features/mobile/auth/hooks/index.ts.ejs +1 -0
- package/templates/features/mobile/auth/hooks/useAuth.ts.ejs +367 -0
- package/templates/features/mobile/auth/services/deviceSession.ts +370 -0
- package/templates/features/mobile/auth/store/deviceSession.store.ts +326 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/_layout.tsx.ejs +11 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-1.tsx.ejs +52 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-2.tsx.ejs +52 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-3.tsx.ejs +60 -0
- package/templates/features/mobile/paywall/app/paywall.tsx +550 -0
- package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx +26 -0
- package/templates/features/mobile/tabs/app/(tabs)/index.tsx +565 -0
- package/templates/features/web/.gitkeep +0 -0
- package/templates/features/web/auth/app/(app)/dashboard/dashboard-client.tsx.ejs +166 -0
- package/templates/features/web/auth/app/(app)/dashboard/page.tsx.ejs +24 -0
- package/templates/features/web/auth/app/(app)/layout.tsx.ejs +43 -0
- package/templates/features/web/auth/app/(app)/settings/sessions/page.tsx.ejs +29 -0
- package/templates/features/web/auth/app/(app)/settings/sessions/sessions-client.tsx.ejs +77 -0
- package/templates/features/web/auth/app/(auth)/forgot-password/page.tsx.ejs +127 -0
- package/templates/features/web/auth/app/(auth)/layout.tsx.ejs +32 -0
- package/templates/features/web/auth/app/(auth)/login/page.tsx.ejs +35 -0
- package/templates/features/web/auth/app/(auth)/register/page.tsx.ejs +19 -0
- package/templates/features/web/auth/app/(auth)/reset-password/page.tsx.ejs +40 -0
- package/templates/features/web/auth/app/(auth)/verify-email/page.tsx.ejs +198 -0
- package/templates/features/web/auth/app/auth/callback/route.ts.ejs +152 -0
- package/templates/features/web/auth/components/auth/login-form.tsx.ejs +100 -0
- package/templates/features/web/auth/components/auth/oauth-buttons.tsx.ejs +126 -0
- package/templates/features/web/auth/components/auth/password-reset-form.tsx.ejs +103 -0
- package/templates/features/web/auth/components/auth/register-form.tsx.ejs +139 -0
- package/templates/features/web/auth/components/settings/session-card.tsx.ejs +132 -0
- package/templates/integrations/mobile/adjust/services/adjustService.ts.ejs +163 -0
- package/templates/integrations/mobile/adjust/store/adjust.store.ts +243 -0
- package/templates/integrations/mobile/att/services/attService.ts +84 -0
- package/templates/integrations/mobile/att/services/trackingPermissions.ts +208 -0
- package/templates/integrations/mobile/att/store/att.store.ts +162 -0
- package/templates/integrations/mobile/revenuecat/services/revenuecatService.ts.ejs +174 -0
- package/templates/integrations/mobile/revenuecat/store/revenuecat.store.ts +286 -0
- package/templates/integrations/mobile/scate/services/scateService.ts.ejs +85 -0
- package/templates/integrations/mobile/scate/store/scate.store.ts +125 -0
- package/templates/integrations/web/.gitkeep +0 -0
- package/templates/shared/.env.example.ejs +21 -0
- package/templates/shared/.gitignore.ejs +145 -0
- package/templates/shared/README.md.ejs +134 -0
- package/templates/shared/docker-compose.prod.yml.ejs +120 -0
- package/templates/shared/docker-compose.yml.ejs +129 -0
- package/templates/shared/scripts/docker-dev.sh.ejs +395 -0
- package/templates/shared/scripts/docker-prod.sh.ejs +542 -0
- package/templates/shared/scripts/setup.sh.ejs +979 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import api from './api';
|
|
3
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
4
|
+
import { logger } from '../utils/logger';
|
|
5
|
+
import { Platform } from 'react-native';
|
|
6
|
+
import * as Application from 'expo-application';
|
|
7
|
+
import * as SecureStore from 'expo-secure-store';
|
|
8
|
+
|
|
9
|
+
export interface DeviceSession {
|
|
10
|
+
id: string;
|
|
11
|
+
deviceId: string;
|
|
12
|
+
sessionToken: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
lastActiveAt: string;
|
|
15
|
+
migrated: boolean;
|
|
16
|
+
migratedToUserId?: string;
|
|
17
|
+
preferredCurrency: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CreateDeviceSessionResponse {
|
|
21
|
+
session: DeviceSession;
|
|
22
|
+
sessionToken: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DeviceSessionValidationResponse {
|
|
26
|
+
valid: boolean;
|
|
27
|
+
session?: DeviceSession;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DeviceSessionMigrationEligibilityResponse {
|
|
31
|
+
canMigrate: boolean;
|
|
32
|
+
reason?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class DeviceSessionService {
|
|
36
|
+
private static instance: DeviceSessionService;
|
|
37
|
+
private sessionToken: string | null = null;
|
|
38
|
+
private deviceId: string | null = null;
|
|
39
|
+
private session: DeviceSession | null = null;
|
|
40
|
+
|
|
41
|
+
private readonly SESSION_TOKEN_KEY = 'device_session_token';
|
|
42
|
+
private readonly DEVICE_ID_KEY = 'device_id';
|
|
43
|
+
private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
|
44
|
+
|
|
45
|
+
private constructor() {
|
|
46
|
+
logger.debug('DeviceSessionService: Initializing...');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public static getInstance(): DeviceSessionService {
|
|
50
|
+
if (!DeviceSessionService.instance) {
|
|
51
|
+
DeviceSessionService.instance = new DeviceSessionService();
|
|
52
|
+
}
|
|
53
|
+
return DeviceSessionService.instance;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async initialize(): Promise<DeviceSession | null> {
|
|
57
|
+
try {
|
|
58
|
+
logger.info('SessionService: Initializing session...');
|
|
59
|
+
|
|
60
|
+
// Get or create device ID
|
|
61
|
+
const deviceId = await this.getOrCreateDeviceId();
|
|
62
|
+
|
|
63
|
+
// Check for existing session token
|
|
64
|
+
const existingToken = await this.getSessionToken();
|
|
65
|
+
|
|
66
|
+
if (existingToken) {
|
|
67
|
+
// Validate existing device session
|
|
68
|
+
const validation = await this.validateDeviceSession(existingToken);
|
|
69
|
+
if (validation.valid && validation.session) {
|
|
70
|
+
this.session = validation.session;
|
|
71
|
+
this.sessionToken = existingToken;
|
|
72
|
+
this.startHeartbeat();
|
|
73
|
+
logger.info('DeviceSessionService: Restored existing device session');
|
|
74
|
+
return validation.session;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Create new device session if no valid existing session
|
|
79
|
+
const newSession = await this.createDeviceSession(deviceId);
|
|
80
|
+
this.session = newSession.session;
|
|
81
|
+
this.sessionToken = newSession.sessionToken;
|
|
82
|
+
await this.setSessionToken(newSession.sessionToken);
|
|
83
|
+
this.startHeartbeat();
|
|
84
|
+
|
|
85
|
+
logger.info('SessionService: Created new session');
|
|
86
|
+
return newSession.session;
|
|
87
|
+
|
|
88
|
+
} catch (error) {
|
|
89
|
+
logger.error('SessionService: Failed to initialize session', { error });
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async createDeviceSession(deviceId: string): Promise<CreateDeviceSessionResponse> {
|
|
95
|
+
try {
|
|
96
|
+
logger.info('DeviceSessionService: Creating new device session for device:', deviceId);
|
|
97
|
+
|
|
98
|
+
const response = await api.post<CreateDeviceSessionResponse>('/device-sessions', { deviceId });
|
|
99
|
+
|
|
100
|
+
logger.info('DeviceSessionService: Device session created successfully');
|
|
101
|
+
return response.data;
|
|
102
|
+
|
|
103
|
+
} catch (error) {
|
|
104
|
+
logger.error('SessionService: Failed to create session', { error });
|
|
105
|
+
|
|
106
|
+
if (axios.isAxiosError(error)) {
|
|
107
|
+
throw new Error(error.response?.data?.error?.message || 'Failed to create session');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
throw new Error('Session creation failed');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async validateDeviceSession(sessionToken: string): Promise<DeviceSessionValidationResponse> {
|
|
115
|
+
try {
|
|
116
|
+
logger.debug('DeviceSessionService: Validating device session token...');
|
|
117
|
+
|
|
118
|
+
const response = await api.post<DeviceSessionValidationResponse>('/device-sessions/validate', { sessionToken });
|
|
119
|
+
|
|
120
|
+
logger.debug('DeviceSessionService: Device session validation complete');
|
|
121
|
+
return response.data;
|
|
122
|
+
|
|
123
|
+
} catch (error) {
|
|
124
|
+
logger.error('SessionService: Failed to validate session', { error });
|
|
125
|
+
|
|
126
|
+
// Return invalid for validation errors
|
|
127
|
+
return { valid: false };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async updateActivity(): Promise<void> {
|
|
132
|
+
try {
|
|
133
|
+
const token = await this.getSessionToken();
|
|
134
|
+
if (!token) {
|
|
135
|
+
logger.warn('SessionService: No session token for activity update');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
logger.debug('SessionService: Updating session activity...');
|
|
140
|
+
|
|
141
|
+
await api.put('/device-sessions/activity', { sessionToken: token });
|
|
142
|
+
|
|
143
|
+
logger.debug('SessionService: Session activity updated');
|
|
144
|
+
|
|
145
|
+
} catch (error) {
|
|
146
|
+
logger.error('SessionService: Failed to update session activity', { error });
|
|
147
|
+
|
|
148
|
+
// Don't throw for activity update errors
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async getMigrationEligibility(): Promise<DeviceSessionMigrationEligibilityResponse> {
|
|
153
|
+
try {
|
|
154
|
+
const token = await this.getSessionToken();
|
|
155
|
+
if (!token) {
|
|
156
|
+
throw new Error('No device session token available');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
logger.debug('DeviceSessionService: Checking migration eligibility...');
|
|
160
|
+
|
|
161
|
+
const response = await api.post<DeviceSessionMigrationEligibilityResponse>('/device-sessions/migration-eligibility', { sessionToken: token });
|
|
162
|
+
|
|
163
|
+
logger.debug('DeviceSessionService: Migration eligibility checked');
|
|
164
|
+
return response.data;
|
|
165
|
+
|
|
166
|
+
} catch (error) {
|
|
167
|
+
logger.error('SessionService: Failed to check migration eligibility', { error });
|
|
168
|
+
|
|
169
|
+
if (axios.isAxiosError(error)) {
|
|
170
|
+
throw new Error(error.response?.data?.error?.message || 'Failed to check migration eligibility');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
throw new Error('Migration eligibility check failed');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async deleteSession(): Promise<void> {
|
|
178
|
+
try {
|
|
179
|
+
const token = await this.getSessionToken();
|
|
180
|
+
if (!token) {
|
|
181
|
+
logger.warn('SessionService: No session token to delete');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
logger.info('SessionService: Deleting session...');
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
await api.delete('/device-sessions', { data: { sessionToken: token } });
|
|
189
|
+
} catch (error) {
|
|
190
|
+
logger.warn('SessionService: Session deletion API call failed, continuing with local cleanup', { error });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Always clear local session data
|
|
194
|
+
await this.clearSession();
|
|
195
|
+
logger.info('SessionService: Session deleted successfully');
|
|
196
|
+
|
|
197
|
+
} catch (error) {
|
|
198
|
+
logger.error('SessionService: Error during session deletion', { error });
|
|
199
|
+
// Still try to clear local data on error
|
|
200
|
+
await this.clearSession();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Device ID management
|
|
205
|
+
private async getOrCreateDeviceId(): Promise<string> {
|
|
206
|
+
try {
|
|
207
|
+
// Try to get existing device ID from secure storage
|
|
208
|
+
let deviceId = await SecureStore.getItemAsync(this.DEVICE_ID_KEY);
|
|
209
|
+
|
|
210
|
+
if (deviceId) {
|
|
211
|
+
this.deviceId = deviceId;
|
|
212
|
+
logger.debug('SessionService: Found existing device ID');
|
|
213
|
+
return deviceId;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Generate new device ID
|
|
217
|
+
deviceId = await this.generateDeviceId();
|
|
218
|
+
await SecureStore.setItemAsync(this.DEVICE_ID_KEY, deviceId);
|
|
219
|
+
this.deviceId = deviceId;
|
|
220
|
+
|
|
221
|
+
logger.info('SessionService: Generated new device ID');
|
|
222
|
+
return deviceId;
|
|
223
|
+
|
|
224
|
+
} catch (error) {
|
|
225
|
+
logger.error('SessionService: Failed to get/create device ID', { error });
|
|
226
|
+
|
|
227
|
+
// Fallback to AsyncStorage if SecureStore fails
|
|
228
|
+
try {
|
|
229
|
+
let deviceId = await AsyncStorage.getItem(this.DEVICE_ID_KEY);
|
|
230
|
+
|
|
231
|
+
if (deviceId) {
|
|
232
|
+
this.deviceId = deviceId;
|
|
233
|
+
return deviceId;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
deviceId = await this.generateDeviceId();
|
|
237
|
+
await AsyncStorage.setItem(this.DEVICE_ID_KEY, deviceId);
|
|
238
|
+
this.deviceId = deviceId;
|
|
239
|
+
|
|
240
|
+
return deviceId;
|
|
241
|
+
} catch (fallbackError) {
|
|
242
|
+
logger.error('SessionService: Fallback device ID generation failed', { fallbackError });
|
|
243
|
+
throw new Error('Failed to generate device ID');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private async generateDeviceId(): Promise<string> {
|
|
249
|
+
try {
|
|
250
|
+
// Create a unique device identifier
|
|
251
|
+
const installId = Application.applicationId || 'unknown';
|
|
252
|
+
const buildId = Application.nativeBuildVersion || '0';
|
|
253
|
+
const platform = Platform.OS;
|
|
254
|
+
const timestamp = Date.now().toString();
|
|
255
|
+
const randomBytes = Math.random().toString(36).substring(2, 15);
|
|
256
|
+
|
|
257
|
+
const deviceId = `${platform}-${installId.replace(/\./g, '_')}-${buildId}-${timestamp}-${randomBytes}`;
|
|
258
|
+
|
|
259
|
+
logger.debug('SessionService: Generated device ID pattern:', deviceId.substring(0, 20) + '...');
|
|
260
|
+
return deviceId;
|
|
261
|
+
|
|
262
|
+
} catch (error) {
|
|
263
|
+
logger.error('SessionService: Failed to generate device ID', { error });
|
|
264
|
+
|
|
265
|
+
// Fallback to simple random ID
|
|
266
|
+
const fallbackId = `${Platform.OS}-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
267
|
+
logger.warn('SessionService: Using fallback device ID');
|
|
268
|
+
return fallbackId;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Session token management
|
|
273
|
+
public async getSessionToken(): Promise<string | null> {
|
|
274
|
+
if (this.sessionToken) {
|
|
275
|
+
return this.sessionToken;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const token = await AsyncStorage.getItem(this.SESSION_TOKEN_KEY);
|
|
279
|
+
this.sessionToken = token;
|
|
280
|
+
return token;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
logger.error('SessionService: Failed to get session token from storage', { error });
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private async setSessionToken(token: string): Promise<void> {
|
|
288
|
+
try {
|
|
289
|
+
logger.debug('SessionService: Setting session token...');
|
|
290
|
+
await AsyncStorage.setItem(this.SESSION_TOKEN_KEY, token);
|
|
291
|
+
this.sessionToken = token;
|
|
292
|
+
logger.debug('SessionService: Session token set successfully');
|
|
293
|
+
} catch (error) {
|
|
294
|
+
logger.error('SessionService: Failed to save session token to storage', { error });
|
|
295
|
+
throw new Error('Failed to save session token');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private async removeSessionToken(): Promise<void> {
|
|
300
|
+
try {
|
|
301
|
+
logger.debug('SessionService: Removing session token...');
|
|
302
|
+
await AsyncStorage.removeItem(this.SESSION_TOKEN_KEY);
|
|
303
|
+
this.sessionToken = null;
|
|
304
|
+
logger.debug('SessionService: Session token removed successfully');
|
|
305
|
+
} catch (error) {
|
|
306
|
+
logger.error('SessionService: Failed to remove session token from storage', { error });
|
|
307
|
+
throw new Error('Failed to remove session token');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async clearSession(): Promise<void> {
|
|
312
|
+
try {
|
|
313
|
+
logger.info('SessionService: Clearing session...');
|
|
314
|
+
this.stopHeartbeat();
|
|
315
|
+
await this.removeSessionToken();
|
|
316
|
+
this.session = null;
|
|
317
|
+
logger.info('SessionService: Session cleared successfully');
|
|
318
|
+
} catch (error) {
|
|
319
|
+
logger.error('SessionService: Failed to clear session', { error });
|
|
320
|
+
throw new Error('Failed to clear session');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Device session state getters
|
|
325
|
+
getCurrentDeviceSession(): DeviceSession | null {
|
|
326
|
+
return this.session;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async isDeviceSessionValid(): Promise<boolean> {
|
|
330
|
+
try {
|
|
331
|
+
const token = await this.getSessionToken();
|
|
332
|
+
if (!token) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const validation = await this.validateDeviceSession(token);
|
|
337
|
+
return validation.valid;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
logger.error('DeviceSessionService: Error checking device session validity:', error);
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
getDeviceId(): string | null {
|
|
345
|
+
return this.deviceId;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Heartbeat management
|
|
349
|
+
private startHeartbeat(): void {
|
|
350
|
+
// Clear any existing heartbeat
|
|
351
|
+
this.stopHeartbeat();
|
|
352
|
+
|
|
353
|
+
// Update activity every 5 minutes
|
|
354
|
+
this.heartbeatInterval = setInterval(() => {
|
|
355
|
+
this.updateActivity();
|
|
356
|
+
}, 5 * 60 * 1000);
|
|
357
|
+
|
|
358
|
+
logger.debug('SessionService: Heartbeat started');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private stopHeartbeat(): void {
|
|
362
|
+
if (this.heartbeatInterval) {
|
|
363
|
+
clearInterval(this.heartbeatInterval);
|
|
364
|
+
this.heartbeatInterval = null;
|
|
365
|
+
logger.debug('SessionService: Heartbeat stopped');
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export const deviceSessionService = DeviceSessionService.getInstance();
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
3
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
4
|
+
import { deviceSessionService, DeviceSession, DeviceSessionMigrationEligibilityResponse } from '../services/deviceSession';
|
|
5
|
+
import { logger } from '../utils/logger';
|
|
6
|
+
|
|
7
|
+
export interface MigrateSessionData {
|
|
8
|
+
name: string;
|
|
9
|
+
email: string;
|
|
10
|
+
password: string;
|
|
11
|
+
passwordConfirmation: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SessionMigrationResponse {
|
|
15
|
+
user: {
|
|
16
|
+
id: string;
|
|
17
|
+
email: string;
|
|
18
|
+
name: string;
|
|
19
|
+
preferredCurrency: string;
|
|
20
|
+
createdAt: string;
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
};
|
|
23
|
+
token: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface SessionState {
|
|
27
|
+
// State
|
|
28
|
+
session: DeviceSession | null;
|
|
29
|
+
sessionToken: string | null;
|
|
30
|
+
deviceId: string | null;
|
|
31
|
+
isLoading: boolean;
|
|
32
|
+
isSessionChecked: boolean;
|
|
33
|
+
error: string | null;
|
|
34
|
+
|
|
35
|
+
// Actions
|
|
36
|
+
setSession: (session: DeviceSession | null) => void;
|
|
37
|
+
setSessionToken: (token: string | null) => void;
|
|
38
|
+
setDeviceId: (deviceId: string | null) => void;
|
|
39
|
+
setLoading: (loading: boolean) => void;
|
|
40
|
+
setError: (error: string | null) => void;
|
|
41
|
+
setSessionChecked: (checked: boolean) => void;
|
|
42
|
+
clearSession: () => void;
|
|
43
|
+
clearError: () => void;
|
|
44
|
+
|
|
45
|
+
// Async actions
|
|
46
|
+
initializeSession: () => Promise<void>;
|
|
47
|
+
deleteSession: () => Promise<void>;
|
|
48
|
+
isSessionValid: () => Promise<boolean>;
|
|
49
|
+
getMigrationEligibility: () => Promise<DeviceSessionMigrationEligibilityResponse>;
|
|
50
|
+
migrateSession: (data: MigrateSessionData) => Promise<SessionMigrationResponse>;
|
|
51
|
+
refreshSession: () => Promise<void>;
|
|
52
|
+
|
|
53
|
+
// Computed values
|
|
54
|
+
hasValidSession: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const useSessionStore = create<SessionState>()(
|
|
58
|
+
(set, get) => ({
|
|
59
|
+
// Initial state
|
|
60
|
+
session: null,
|
|
61
|
+
sessionToken: null,
|
|
62
|
+
deviceId: null,
|
|
63
|
+
isLoading: false,
|
|
64
|
+
isSessionChecked: false,
|
|
65
|
+
error: null,
|
|
66
|
+
|
|
67
|
+
// Actions
|
|
68
|
+
setSession: (session) => set({ session }),
|
|
69
|
+
setSessionToken: (sessionToken) => set({ sessionToken }),
|
|
70
|
+
setDeviceId: (deviceId) => set({ deviceId }),
|
|
71
|
+
setLoading: (isLoading) => set({ isLoading }),
|
|
72
|
+
setError: (error) => set({ error }),
|
|
73
|
+
setSessionChecked: (isSessionChecked) => set({ isSessionChecked }),
|
|
74
|
+
|
|
75
|
+
clearSession: () => set({
|
|
76
|
+
session: null,
|
|
77
|
+
sessionToken: null,
|
|
78
|
+
deviceId: null,
|
|
79
|
+
error: null,
|
|
80
|
+
isLoading: false
|
|
81
|
+
}),
|
|
82
|
+
|
|
83
|
+
clearError: () => set({ error: null }),
|
|
84
|
+
|
|
85
|
+
// Async actions
|
|
86
|
+
initializeSession: async () => {
|
|
87
|
+
try {
|
|
88
|
+
logger.debug('SessionStore: Initializing session...');
|
|
89
|
+
set({ isLoading: true, error: null });
|
|
90
|
+
|
|
91
|
+
const sessionData = await deviceSessionService.initialize();
|
|
92
|
+
|
|
93
|
+
if (sessionData) {
|
|
94
|
+
set({
|
|
95
|
+
session: sessionData,
|
|
96
|
+
sessionToken: await deviceSessionService.getSessionToken(),
|
|
97
|
+
deviceId: deviceSessionService.getDeviceId(),
|
|
98
|
+
});
|
|
99
|
+
logger.info('SessionStore: Session initialized successfully', {
|
|
100
|
+
sessionId: sessionData.id,
|
|
101
|
+
deviceId: sessionData.deviceId
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
logger.warn('SessionStore: No session data returned from initialize');
|
|
105
|
+
set({ session: null, sessionToken: null });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logger.error('SessionStore: Failed to initialize session', { error });
|
|
110
|
+
|
|
111
|
+
const errorMessage = error instanceof Error ? error.message : 'Session initialization failed';
|
|
112
|
+
set({ error: errorMessage, session: null, sessionToken: null });
|
|
113
|
+
|
|
114
|
+
} finally {
|
|
115
|
+
set({ isLoading: false, isSessionChecked: true });
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
deleteSession: async () => {
|
|
120
|
+
try {
|
|
121
|
+
logger.info('SessionStore: Deleting session...');
|
|
122
|
+
set({ isLoading: true, error: null });
|
|
123
|
+
|
|
124
|
+
// Clear onboarding flag to restart onboarding flow
|
|
125
|
+
try {
|
|
126
|
+
await AsyncStorage.removeItem('onboarding_completed');
|
|
127
|
+
logger.info('Onboarding flag cleared after session deletion');
|
|
128
|
+
} catch (error) {
|
|
129
|
+
logger.error('Failed to clear onboarding flag', { error });
|
|
130
|
+
// Don't throw - not critical
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await deviceSessionService.deleteSession();
|
|
134
|
+
set({ session: null, sessionToken: null, deviceId: null });
|
|
135
|
+
|
|
136
|
+
logger.info('SessionStore: Session deleted successfully');
|
|
137
|
+
|
|
138
|
+
} catch (error) {
|
|
139
|
+
// Still clear onboarding flag even on error
|
|
140
|
+
try {
|
|
141
|
+
await AsyncStorage.removeItem('onboarding_completed');
|
|
142
|
+
} catch (e) {
|
|
143
|
+
logger.error('Failed to clear onboarding flag in error handler', { e });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
logger.error('SessionStore: Failed to delete session', { error });
|
|
147
|
+
|
|
148
|
+
const errorMessage = error instanceof Error ? error.message : 'Session deletion failed';
|
|
149
|
+
set({ error: errorMessage });
|
|
150
|
+
|
|
151
|
+
throw error;
|
|
152
|
+
|
|
153
|
+
} finally {
|
|
154
|
+
set({ isLoading: false });
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
isSessionValid: async () => {
|
|
159
|
+
try {
|
|
160
|
+
logger.debug('SessionStore: Checking session validity...');
|
|
161
|
+
|
|
162
|
+
const isValid = await deviceSessionService.isDeviceSessionValid();
|
|
163
|
+
|
|
164
|
+
if (!isValid && get().session) {
|
|
165
|
+
// Session became invalid, clear it
|
|
166
|
+
logger.warn('SessionStore: Session is no longer valid, clearing');
|
|
167
|
+
set({ session: null, sessionToken: null, error: 'Session expired' });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return isValid;
|
|
171
|
+
|
|
172
|
+
} catch (error) {
|
|
173
|
+
logger.error('SessionStore: Error checking session validity', { error });
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
getMigrationEligibility: async () => {
|
|
179
|
+
try {
|
|
180
|
+
logger.debug('SessionStore: Checking migration eligibility...');
|
|
181
|
+
set({ error: null });
|
|
182
|
+
|
|
183
|
+
const eligibility = await deviceSessionService.getMigrationEligibility();
|
|
184
|
+
|
|
185
|
+
logger.debug('SessionStore: Migration eligibility checked', {
|
|
186
|
+
canMigrate: eligibility.canMigrate
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return eligibility;
|
|
190
|
+
|
|
191
|
+
} catch (error) {
|
|
192
|
+
logger.error('SessionStore: Failed to check migration eligibility', { error });
|
|
193
|
+
|
|
194
|
+
const errorMessage = error instanceof Error ? error.message : 'Migration eligibility check failed';
|
|
195
|
+
set({ error: errorMessage });
|
|
196
|
+
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
migrateSession: async (data: MigrateSessionData): Promise<SessionMigrationResponse> => {
|
|
202
|
+
try {
|
|
203
|
+
logger.info('SessionStore: Migrating session to user account...');
|
|
204
|
+
set({ isLoading: true, error: null });
|
|
205
|
+
|
|
206
|
+
const sessionToken = get().sessionToken;
|
|
207
|
+
if (!sessionToken) {
|
|
208
|
+
throw new Error('No session token available for migration');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Call the migration endpoint
|
|
212
|
+
const migrationData = {
|
|
213
|
+
sessionToken,
|
|
214
|
+
...data,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// This would be a direct API call since migration is a one-time operation
|
|
218
|
+
const response = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/sessions/migrate`, {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: {
|
|
221
|
+
'Content-Type': 'application/json',
|
|
222
|
+
},
|
|
223
|
+
body: JSON.stringify(migrationData),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (!response.ok) {
|
|
227
|
+
const errorData = await response.json() as { error?: { message?: string } };
|
|
228
|
+
throw new Error(errorData.error?.message || 'Migration failed');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const result = await response.json() as SessionMigrationResponse;
|
|
232
|
+
|
|
233
|
+
// Clear session data after successful migration
|
|
234
|
+
set({ session: null, sessionToken: null, deviceId: null });
|
|
235
|
+
|
|
236
|
+
logger.info('SessionStore: Session migrated successfully', {
|
|
237
|
+
userId: result.user.id,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return result;
|
|
241
|
+
|
|
242
|
+
} catch (error) {
|
|
243
|
+
logger.error('SessionStore: Failed to migrate session', { error });
|
|
244
|
+
|
|
245
|
+
const errorMessage = error instanceof Error ? error.message : 'Session migration failed';
|
|
246
|
+
set({ error: errorMessage });
|
|
247
|
+
|
|
248
|
+
throw error;
|
|
249
|
+
|
|
250
|
+
} finally {
|
|
251
|
+
set({ isLoading: false });
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
refreshSession: async () => {
|
|
256
|
+
try {
|
|
257
|
+
logger.info('SessionStore: Refreshing session...');
|
|
258
|
+
set({ error: null });
|
|
259
|
+
|
|
260
|
+
// Check if current session is still valid
|
|
261
|
+
const currentSession = deviceSessionService.getCurrentDeviceSession();
|
|
262
|
+
if (currentSession) {
|
|
263
|
+
const isValid = await deviceSessionService.isDeviceSessionValid();
|
|
264
|
+
|
|
265
|
+
if (isValid) {
|
|
266
|
+
// Session is still valid, just update the local state
|
|
267
|
+
set({ session: currentSession });
|
|
268
|
+
logger.debug('SessionStore: Session is still valid');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Session is invalid or doesn't exist, initialize a new one
|
|
274
|
+
await get().initializeSession();
|
|
275
|
+
|
|
276
|
+
} catch (error) {
|
|
277
|
+
logger.error('SessionStore: Failed to refresh session', { error });
|
|
278
|
+
|
|
279
|
+
const errorMessage = error instanceof Error ? error.message : 'Session refresh failed';
|
|
280
|
+
set({ error: errorMessage });
|
|
281
|
+
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
// Computed values
|
|
287
|
+
get hasValidSession() {
|
|
288
|
+
const state = get();
|
|
289
|
+
return !!(state.session && state.sessionToken);
|
|
290
|
+
},
|
|
291
|
+
})
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// Selectors for commonly used combinations
|
|
295
|
+
export const useSession = () => {
|
|
296
|
+
const state = useSessionStore();
|
|
297
|
+
return {
|
|
298
|
+
session: state.session,
|
|
299
|
+
sessionToken: state.sessionToken,
|
|
300
|
+
deviceId: state.deviceId,
|
|
301
|
+
isLoading: state.isLoading,
|
|
302
|
+
isSessionChecked: state.isSessionChecked,
|
|
303
|
+
error: state.error,
|
|
304
|
+
hasValidSession: state.hasValidSession,
|
|
305
|
+
clearError: state.clearError,
|
|
306
|
+
};
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
export const useSessionActions = () => {
|
|
310
|
+
const state = useSessionStore();
|
|
311
|
+
return {
|
|
312
|
+
setSession: state.setSession,
|
|
313
|
+
setSessionToken: state.setSessionToken,
|
|
314
|
+
setDeviceId: state.setDeviceId,
|
|
315
|
+
setLoading: state.setLoading,
|
|
316
|
+
setError: state.setError,
|
|
317
|
+
setSessionChecked: state.setSessionChecked,
|
|
318
|
+
clearSession: state.clearSession,
|
|
319
|
+
initializeSession: state.initializeSession,
|
|
320
|
+
deleteSession: state.deleteSession,
|
|
321
|
+
isSessionValid: state.isSessionValid,
|
|
322
|
+
getMigrationEligibility: state.getMigrationEligibility,
|
|
323
|
+
migrateSession: state.migrateSession,
|
|
324
|
+
refreshSession: state.refreshSession,
|
|
325
|
+
};
|
|
326
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Stack } from 'expo-router';
|
|
2
|
+
|
|
3
|
+
export default function OnboardingLayout() {
|
|
4
|
+
return (
|
|
5
|
+
<Stack screenOptions={{ headerShown: false }}>
|
|
6
|
+
<% for (let i = 1; i <= features.onboarding.pages && i <= 3; i++) { %>
|
|
7
|
+
<Stack.Screen name="page-<%= i %>" />
|
|
8
|
+
<% } %>
|
|
9
|
+
</Stack>
|
|
10
|
+
);
|
|
11
|
+
}
|