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,279 @@
|
|
|
1
|
+
import Redis from 'ioredis';
|
|
2
|
+
import { REDIS_KEYS, TTL } from '../lib/constants';
|
|
3
|
+
|
|
4
|
+
const redisHost = process.env.REDIS_HOST || 'localhost';
|
|
5
|
+
const redisPort = parseInt(process.env.REDIS_PORT || '6379', 10);
|
|
6
|
+
const redisPassword = process.env.REDIS_PASSWORD;
|
|
7
|
+
const redisDb = parseInt(process.env.REDIS_DB || '0', 10);
|
|
8
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// In-Memory Store (Development Fallback Only)
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// This store is ONLY used when Redis is unavailable in development.
|
|
14
|
+
// In production, Redis unavailability will cause OAuth to fail (as intended).
|
|
15
|
+
|
|
16
|
+
interface MemoryStoreEntry {
|
|
17
|
+
data: string;
|
|
18
|
+
expiresAt: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class InMemoryStore {
|
|
22
|
+
private store = new Map<string, MemoryStoreEntry>();
|
|
23
|
+
private cleanupInterval: ReturnType<typeof setInterval> | null = null;
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
// Cleanup expired entries every 60 seconds
|
|
27
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 60000);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async setex(key: string, ttlSeconds: number, value: string): Promise<void> {
|
|
31
|
+
this.store.set(key, {
|
|
32
|
+
data: value,
|
|
33
|
+
expiresAt: Date.now() + ttlSeconds * 1000,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async get(key: string): Promise<string | null> {
|
|
38
|
+
const entry = this.store.get(key);
|
|
39
|
+
if (!entry) return null;
|
|
40
|
+
if (Date.now() > entry.expiresAt) {
|
|
41
|
+
this.store.delete(key);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return entry.data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async del(key: string): Promise<void> {
|
|
48
|
+
this.store.delete(key);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async getdel(key: string): Promise<string | null> {
|
|
52
|
+
const value = await this.get(key);
|
|
53
|
+
if (value) {
|
|
54
|
+
this.store.delete(key);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Lua script simulation for atomic get-and-delete
|
|
60
|
+
async eval(_script: string, _numKeys: number, ...args: string[]): Promise<string | null> {
|
|
61
|
+
// For our use case, this is always the atomic get-delete pattern
|
|
62
|
+
const key = args[0];
|
|
63
|
+
return this.getdel(key);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private cleanup(): void {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
for (const [key, entry] of this.store.entries()) {
|
|
69
|
+
if (now > entry.expiresAt) {
|
|
70
|
+
this.store.delete(key);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
destroy(): void {
|
|
76
|
+
if (this.cleanupInterval) {
|
|
77
|
+
clearInterval(this.cleanupInterval);
|
|
78
|
+
}
|
|
79
|
+
this.store.clear();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// =============================================================================
|
|
84
|
+
// Redis Client
|
|
85
|
+
// =============================================================================
|
|
86
|
+
|
|
87
|
+
let redis: Redis | null = null;
|
|
88
|
+
let redisAvailable = false;
|
|
89
|
+
let memoryStore: InMemoryStore | null = null;
|
|
90
|
+
let usingMemoryFallback = false;
|
|
91
|
+
|
|
92
|
+
function createRedisClient(): Redis {
|
|
93
|
+
const client = new Redis({
|
|
94
|
+
host: redisHost,
|
|
95
|
+
port: redisPort,
|
|
96
|
+
password: redisPassword || undefined,
|
|
97
|
+
db: redisDb,
|
|
98
|
+
maxRetriesPerRequest: 3,
|
|
99
|
+
retryStrategy(times) {
|
|
100
|
+
if (times > 3) return null; // Stop retrying after 3 attempts
|
|
101
|
+
return Math.min(times * 100, 3000); // Exponential backoff
|
|
102
|
+
},
|
|
103
|
+
enableOfflineQueue: false, // Don't queue commands when disconnected
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
client.on('error', (err) => {
|
|
107
|
+
console.error('Redis connection error:', err.message);
|
|
108
|
+
redisAvailable = false;
|
|
109
|
+
|
|
110
|
+
// In development, activate memory fallback when Redis fails
|
|
111
|
+
if (isDevelopment && !usingMemoryFallback) {
|
|
112
|
+
console.warn('⚠️ Redis unavailable in development - using in-memory store fallback');
|
|
113
|
+
console.warn('⚠️ OAuth state will NOT persist across server restarts!');
|
|
114
|
+
usingMemoryFallback = true;
|
|
115
|
+
if (!memoryStore) {
|
|
116
|
+
memoryStore = new InMemoryStore();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
client.on('connect', () => {
|
|
122
|
+
console.log('Connected to Redis');
|
|
123
|
+
redisAvailable = true;
|
|
124
|
+
usingMemoryFallback = false;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
client.on('close', () => {
|
|
128
|
+
redisAvailable = false;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return client;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Initialize Redis client
|
|
135
|
+
export async function initRedis(): Promise<void> {
|
|
136
|
+
if (!redis) {
|
|
137
|
+
redis = createRedisClient();
|
|
138
|
+
try {
|
|
139
|
+
await redis.ping();
|
|
140
|
+
redisAvailable = true;
|
|
141
|
+
console.log('✅ Redis connected successfully');
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error('Failed to connect to Redis:', (err as Error).message);
|
|
144
|
+
redisAvailable = false;
|
|
145
|
+
|
|
146
|
+
if (isDevelopment) {
|
|
147
|
+
console.warn('⚠️ Using in-memory store fallback for development');
|
|
148
|
+
usingMemoryFallback = true;
|
|
149
|
+
memoryStore = new InMemoryStore();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Get the active store (Redis or in-memory fallback)
|
|
156
|
+
function getStore(): Redis | InMemoryStore {
|
|
157
|
+
if (redisAvailable && redis) {
|
|
158
|
+
return redis;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (isDevelopment && usingMemoryFallback && memoryStore) {
|
|
162
|
+
return memoryStore;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
throw new Error('Redis is not available');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check if Redis (or fallback) is available
|
|
169
|
+
export function isRedisAvailable(): boolean {
|
|
170
|
+
return redisAvailable || (isDevelopment && usingMemoryFallback);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check if using in-memory fallback
|
|
174
|
+
export function isUsingMemoryFallback(): boolean {
|
|
175
|
+
return usingMemoryFallback;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* OAuth store helpers with graceful error handling
|
|
180
|
+
* Uses Redis when available, falls back to in-memory store in development
|
|
181
|
+
*/
|
|
182
|
+
export const oauthStore = {
|
|
183
|
+
/**
|
|
184
|
+
* Store PKCE challenge during OAuth init
|
|
185
|
+
* @throws if store is unavailable (Redis down in production)
|
|
186
|
+
*/
|
|
187
|
+
async storeOAuthState(
|
|
188
|
+
state: string,
|
|
189
|
+
data: {
|
|
190
|
+
code_challenge: string;
|
|
191
|
+
callback_url: string;
|
|
192
|
+
provider: string;
|
|
193
|
+
}
|
|
194
|
+
): Promise<void> {
|
|
195
|
+
const store = getStore();
|
|
196
|
+
await store.setex(
|
|
197
|
+
`${REDIS_KEYS.OAUTH_WEB_STATE}${state}`,
|
|
198
|
+
TTL.OAUTH_STATE,
|
|
199
|
+
JSON.stringify({ ...data, created_at: Date.now() })
|
|
200
|
+
);
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Retrieve OAuth state (does NOT delete - caller should delete after use)
|
|
205
|
+
*/
|
|
206
|
+
async getOAuthState(state: string): Promise<{
|
|
207
|
+
code_challenge: string;
|
|
208
|
+
callback_url: string;
|
|
209
|
+
provider: string;
|
|
210
|
+
created_at: number;
|
|
211
|
+
} | null> {
|
|
212
|
+
const store = getStore();
|
|
213
|
+
const data = await store.get(`${REDIS_KEYS.OAUTH_WEB_STATE}${state}`);
|
|
214
|
+
if (!data) return null;
|
|
215
|
+
return JSON.parse(data);
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Delete OAuth state after use
|
|
220
|
+
*/
|
|
221
|
+
async deleteOAuthState(state: string): Promise<void> {
|
|
222
|
+
const store = getStore();
|
|
223
|
+
await store.del(`${REDIS_KEYS.OAUTH_WEB_STATE}${state}`);
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Store exchange token after OAuth callback
|
|
228
|
+
*/
|
|
229
|
+
async storeExchangeToken(
|
|
230
|
+
token: string,
|
|
231
|
+
data: {
|
|
232
|
+
session_token: string;
|
|
233
|
+
code_challenge: string;
|
|
234
|
+
}
|
|
235
|
+
): Promise<void> {
|
|
236
|
+
const store = getStore();
|
|
237
|
+
await store.setex(
|
|
238
|
+
`${REDIS_KEYS.OAUTH_EXCHANGE}${token}`,
|
|
239
|
+
TTL.EXCHANGE_TOKEN,
|
|
240
|
+
JSON.stringify(data)
|
|
241
|
+
);
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Retrieve and delete exchange token (single use)
|
|
246
|
+
* Uses atomic operations to prevent race conditions
|
|
247
|
+
*/
|
|
248
|
+
async consumeExchangeToken(token: string): Promise<{
|
|
249
|
+
session_token: string;
|
|
250
|
+
code_challenge: string;
|
|
251
|
+
} | null> {
|
|
252
|
+
const store = getStore();
|
|
253
|
+
const key = `${REDIS_KEYS.OAUTH_EXCHANGE}${token}`;
|
|
254
|
+
|
|
255
|
+
// Use GETDEL for atomic get-and-delete (Redis 6.2+ / in-memory store)
|
|
256
|
+
let data: string | null = null;
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
data = await store.getdel(key);
|
|
260
|
+
} catch {
|
|
261
|
+
// Fallback for older Redis versions using Lua script for atomicity
|
|
262
|
+
// This ensures the token can only be consumed once even under concurrent requests
|
|
263
|
+
const luaScript = `
|
|
264
|
+
local value = redis.call('GET', KEYS[1])
|
|
265
|
+
if value then
|
|
266
|
+
redis.call('DEL', KEYS[1])
|
|
267
|
+
end
|
|
268
|
+
return value
|
|
269
|
+
`;
|
|
270
|
+
data = (await store.eval(luaScript, 1, key)) as string | null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!data) return null;
|
|
274
|
+
return JSON.parse(data);
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// Export for direct Redis access if needed (use getStore() for fallback support)
|
|
279
|
+
export { redis, getStore };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# IMPORTANT: SDK API Keys Configuration
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# SDK API keys (RevenueCat, Adjust, Scate) are configured in app.json and
|
|
5
|
+
# embedded at build time via expo-constants. They are NOT read from this file.
|
|
6
|
+
#
|
|
7
|
+
# This file is provided for reference and documentation purposes only.
|
|
8
|
+
# The actual keys used by the app come from app.json's "extra" field.
|
|
9
|
+
#
|
|
10
|
+
# To change SDK keys, update app.json and rebuild the app.
|
|
11
|
+
# =============================================================================
|
|
12
|
+
|
|
13
|
+
# Backend API
|
|
14
|
+
API_URL=http://localhost:8080
|
|
15
|
+
|
|
16
|
+
<% if (integrations.revenueCat.enabled) { %># RevenueCat Configuration
|
|
17
|
+
# NOTE: These keys are configured in app.json and embedded at build time
|
|
18
|
+
# Get your API keys from: https://app.revenuecat.com/
|
|
19
|
+
# Reference values (not used at runtime):
|
|
20
|
+
REVENUECAT_IOS_KEY=<%= integrations.revenueCat.iosKey || 'YOUR_IOS_API_KEY_HERE' %>
|
|
21
|
+
REVENUECAT_ANDROID_KEY=<%= integrations.revenueCat.androidKey || 'YOUR_ANDROID_API_KEY_HERE' %>
|
|
22
|
+
<% } %>
|
|
23
|
+
<% if (integrations.adjust.enabled) { %># Adjust Configuration
|
|
24
|
+
# NOTE: These values are configured in app.json and embedded at build time
|
|
25
|
+
# Get your app token from: https://dash.adjust.com/
|
|
26
|
+
# Reference values (not used at runtime):
|
|
27
|
+
ADJUST_APP_TOKEN=<%= integrations.adjust.appToken || 'YOUR_ADJUST_APP_TOKEN_HERE' %>
|
|
28
|
+
ADJUST_ENVIRONMENT=<%= integrations.adjust.environment %>
|
|
29
|
+
<% } %>
|
|
30
|
+
<% if (integrations.scate.enabled) { %># Scate Configuration
|
|
31
|
+
# NOTE: This key is configured in app.json and embedded at build time
|
|
32
|
+
# Get your API key from: https://scate.io/
|
|
33
|
+
# Reference value (not used at runtime):
|
|
34
|
+
SCATE_API_KEY=<%= integrations.scate.apiKey || 'YOUR_SCATE_API_KEY_HERE' %>
|
|
35
|
+
<% } %>
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# React Native/Expo Mobile App - Git Ignore
|
|
2
|
+
|
|
3
|
+
# Dependencies
|
|
4
|
+
node_modules/
|
|
5
|
+
npm-debug.log*
|
|
6
|
+
yarn-debug.log*
|
|
7
|
+
yarn-error.log*
|
|
8
|
+
.pnpm-store/
|
|
9
|
+
|
|
10
|
+
# Expo
|
|
11
|
+
.expo/
|
|
12
|
+
dist/
|
|
13
|
+
web-build/
|
|
14
|
+
expo-env.d.ts
|
|
15
|
+
|
|
16
|
+
# Native build directories and files
|
|
17
|
+
ios/
|
|
18
|
+
android/
|
|
19
|
+
.kotlin/
|
|
20
|
+
|
|
21
|
+
# Metro
|
|
22
|
+
.metro-health-check*
|
|
23
|
+
.metro/
|
|
24
|
+
|
|
25
|
+
# Debug files
|
|
26
|
+
npm-debug.*
|
|
27
|
+
yarn-debug.*
|
|
28
|
+
yarn-error.*
|
|
29
|
+
|
|
30
|
+
# macOS
|
|
31
|
+
.DS_Store
|
|
32
|
+
*.pem
|
|
33
|
+
|
|
34
|
+
# Environment files
|
|
35
|
+
.env
|
|
36
|
+
.env.*
|
|
37
|
+
!.env.example
|
|
38
|
+
|
|
39
|
+
# TypeScript
|
|
40
|
+
*.tsbuildinfo
|
|
41
|
+
tsconfig.tsbuildinfo
|
|
42
|
+
|
|
43
|
+
# EAS
|
|
44
|
+
build/
|
|
45
|
+
*.ipa
|
|
46
|
+
*.apk
|
|
47
|
+
*.aab
|
|
48
|
+
.eas/
|
|
49
|
+
|
|
50
|
+
# React Native
|
|
51
|
+
*.orig.*
|
|
52
|
+
*.jks
|
|
53
|
+
*.p8
|
|
54
|
+
*.p12
|
|
55
|
+
*.key
|
|
56
|
+
*.mobileprovision
|
|
57
|
+
*.keystore
|
|
58
|
+
|
|
59
|
+
# Temporary files
|
|
60
|
+
tmp/
|
|
61
|
+
temp/
|
|
62
|
+
*.tmp
|
|
63
|
+
*.temp
|
|
64
|
+
|
|
65
|
+
# Logs
|
|
66
|
+
logs/
|
|
67
|
+
*.log
|
|
68
|
+
|
|
69
|
+
# Test coverage
|
|
70
|
+
coverage/
|
|
71
|
+
.nyc_output/
|
|
72
|
+
|
|
73
|
+
# Editor and IDE files
|
|
74
|
+
.vscode/
|
|
75
|
+
.idea/
|
|
76
|
+
*.swp
|
|
77
|
+
*.swo
|
|
78
|
+
*~
|
|
79
|
+
|
|
80
|
+
# Cache directories
|
|
81
|
+
.cache/
|
|
82
|
+
.eslintcache
|
|
83
|
+
|
|
84
|
+
# OS files
|
|
85
|
+
.DS_Store
|
|
86
|
+
.DS_Store?
|
|
87
|
+
._*
|
|
88
|
+
.Spotlight-V100
|
|
89
|
+
.Trashes
|
|
90
|
+
ehthumbs.db
|
|
91
|
+
Thumbs.db
|
|
92
|
+
|
|
93
|
+
# Backup files
|
|
94
|
+
*.bak
|
|
95
|
+
*.backup
|
|
96
|
+
*.old
|
|
97
|
+
|
|
98
|
+
# Archive files
|
|
99
|
+
*.zip
|
|
100
|
+
*.tar.gz
|
|
101
|
+
*.rar
|
|
102
|
+
|
|
103
|
+
# Simulator files
|
|
104
|
+
*.trace
|
|
105
|
+
|
|
106
|
+
# Flipper
|
|
107
|
+
Flipper/
|
|
108
|
+
|
|
109
|
+
# CocoaPods (iOS)
|
|
110
|
+
ios/Pods/
|
|
111
|
+
ios/Podfile.lock
|
|
112
|
+
*.xcworkspace
|
|
113
|
+
|
|
114
|
+
# Gradle (Android)
|
|
115
|
+
android/.gradle/
|
|
116
|
+
android/build/
|
|
117
|
+
android/app/build/
|
|
118
|
+
android/local.properties
|
|
119
|
+
|
|
120
|
+
# Fastlane
|
|
121
|
+
fastlane/report.xml
|
|
122
|
+
fastlane/Preview.html
|
|
123
|
+
fastlane/screenshots
|
|
124
|
+
fastlane/test_output
|
|
125
|
+
fastlane/readme.md
|
|
126
|
+
|
|
127
|
+
# Bundle artifacts
|
|
128
|
+
*.jsbundle
|
|
129
|
+
|
|
130
|
+
# Watchman
|
|
131
|
+
.watchmanconfig
|
|
132
|
+
|
|
133
|
+
# Xcode
|
|
134
|
+
*.xcodeproj/*
|
|
135
|
+
!*.xcodeproj/project.pbxproj
|
|
136
|
+
!*.xcodeproj/xcshareddata/
|
|
137
|
+
!*.xcodeproj/project.xcworkspace/
|
|
138
|
+
*.xcworkspace/*
|
|
139
|
+
!*.xcworkspace/contents.xcworkspacedata
|
|
140
|
+
/*.gcno
|
|
141
|
+
|
|
142
|
+
# Android Studio
|
|
143
|
+
.gradle
|
|
144
|
+
/build/
|
|
145
|
+
/captures
|
|
146
|
+
.externalNativeBuild
|
|
147
|
+
|
|
148
|
+
# Detox
|
|
149
|
+
e2e/*.js
|
|
150
|
+
e2e/artifacts/
|
|
151
|
+
|
|
152
|
+
# Storybook
|
|
153
|
+
storybook-static/
|
|
154
|
+
|
|
155
|
+
# Firebase
|
|
156
|
+
.firebase/
|
|
157
|
+
firebase-debug.log
|
|
158
|
+
firebase-debug.*.log
|
|
159
|
+
|
|
160
|
+
# React Native Debugger
|
|
161
|
+
rndebugger_log.txt
|
|
162
|
+
|
|
163
|
+
# CodePush
|
|
164
|
+
CodePush/
|
|
165
|
+
|
|
166
|
+
# App Center
|
|
167
|
+
AppCenter/
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, StyleSheet } from 'react-native';
|
|
3
|
+
import { router } from 'expo-router';
|
|
4
|
+
import { Button } from '../src/components/ui';
|
|
5
|
+
|
|
6
|
+
export default function NotFoundScreen() {
|
|
7
|
+
const handleGoHome = () => {
|
|
8
|
+
router.replace('/(tabs)');
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const handleGoBack = () => {
|
|
12
|
+
if (router.canGoBack()) {
|
|
13
|
+
router.back();
|
|
14
|
+
} else {
|
|
15
|
+
handleGoHome();
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<View style={styles.container}>
|
|
21
|
+
<Text style={styles.title}>404</Text>
|
|
22
|
+
<Text style={styles.message}>Page Not Found</Text>
|
|
23
|
+
<Text style={styles.description}>
|
|
24
|
+
The page you're looking for doesn't exist or has been moved.
|
|
25
|
+
</Text>
|
|
26
|
+
|
|
27
|
+
<View style={styles.actions}>
|
|
28
|
+
<Button
|
|
29
|
+
title="Go Back"
|
|
30
|
+
onPress={handleGoBack}
|
|
31
|
+
style={styles.button}
|
|
32
|
+
/>
|
|
33
|
+
<Button
|
|
34
|
+
title="Go Home"
|
|
35
|
+
variant="outline"
|
|
36
|
+
onPress={handleGoHome}
|
|
37
|
+
style={styles.button}
|
|
38
|
+
/>
|
|
39
|
+
</View>
|
|
40
|
+
</View>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const styles = StyleSheet.create({
|
|
45
|
+
container: {
|
|
46
|
+
flex: 1,
|
|
47
|
+
justifyContent: 'center',
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
padding: 20,
|
|
50
|
+
backgroundColor: '#FFFFFF',
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
title: {
|
|
54
|
+
fontSize: 72,
|
|
55
|
+
fontWeight: 'bold',
|
|
56
|
+
color: '#007AFF',
|
|
57
|
+
marginBottom: 16,
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
message: {
|
|
61
|
+
fontSize: 24,
|
|
62
|
+
fontWeight: '600',
|
|
63
|
+
color: '#000000',
|
|
64
|
+
marginBottom: 8,
|
|
65
|
+
textAlign: 'center',
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
description: {
|
|
69
|
+
fontSize: 16,
|
|
70
|
+
color: '#8E8E93',
|
|
71
|
+
textAlign: 'center',
|
|
72
|
+
lineHeight: 22,
|
|
73
|
+
marginBottom: 32,
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
actions: {
|
|
77
|
+
gap: 12,
|
|
78
|
+
width: '100%',
|
|
79
|
+
maxWidth: 300,
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
button: {
|
|
83
|
+
width: '100%',
|
|
84
|
+
},
|
|
85
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Stack, router } from 'expo-router';
|
|
2
|
+
import { useEffect<% if (features.sessionManagement) { %>, useState<% } %> } from 'react';
|
|
3
|
+
import Constants from 'expo-constants';
|
|
4
|
+
import { ThemeProvider } from '@/context/ThemeContext';
|
|
5
|
+
<% if (features.onboarding.enabled) { %>import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
6
|
+
<% } %><% if (integrations.revenueCat.enabled || integrations.adjust.enabled || integrations.scate.enabled) { %>import { initializeSDKs } from '@/services/sdkInitializer';
|
|
7
|
+
<% } %><% if (features.sessionManagement) { %>import { useSessionActions, useSession } from '../src/store/deviceSession.store';
|
|
8
|
+
<% } %>import { logger } from '../src/utils/logger';
|
|
9
|
+
|
|
10
|
+
export default function RootLayout() {
|
|
11
|
+
<% if (features.sessionManagement) { %> const { initializeSession } = useSessionActions();
|
|
12
|
+
const { isSessionChecked } = useSession();
|
|
13
|
+
<% } else { %> const [isReady, setIsReady] = useState(false);
|
|
14
|
+
<% } %>
|
|
15
|
+
// Get feature flags from app.json
|
|
16
|
+
const onboardingEnabled = Constants.expoConfig?.extra?.features?.onboarding?.enabled ?? false;
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
<% if (integrations.revenueCat.enabled || integrations.adjust.enabled || integrations.scate.enabled) { %> // Initialize SDKs
|
|
20
|
+
initializeSDKs().catch(console.error);
|
|
21
|
+
|
|
22
|
+
<% } %> // App initialization and navigation
|
|
23
|
+
const initializeApp = async () => {
|
|
24
|
+
<% if (features.sessionManagement) { %> // Skip if session already checked
|
|
25
|
+
if (isSessionChecked) {
|
|
26
|
+
logger.debug('RootLayout: Session already checked, skipping');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
<% } %>
|
|
30
|
+
try {
|
|
31
|
+
logger.info('RootLayout: Initializing app...');
|
|
32
|
+
|
|
33
|
+
<% if (features.onboarding.enabled) { %> // Check onboarding completion
|
|
34
|
+
const onboardingCompleted = await AsyncStorage.getItem('onboarding_completed');
|
|
35
|
+
|
|
36
|
+
if (onboardingEnabled && onboardingCompleted !== 'true') {
|
|
37
|
+
// Onboarding not completed - navigate to onboarding
|
|
38
|
+
logger.info('RootLayout: Onboarding not completed, navigating to onboarding');
|
|
39
|
+
router.replace('/(onboarding)/page-1');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
<% } %>
|
|
43
|
+
<% if (features.sessionManagement) { %> // Initialize session
|
|
44
|
+
logger.info('RootLayout: Initializing session');
|
|
45
|
+
await initializeSession();
|
|
46
|
+
<% } %>
|
|
47
|
+
router.replace('/(tabs)');
|
|
48
|
+
} catch (error) {
|
|
49
|
+
logger.error('RootLayout: Failed to initialize app', { error });
|
|
50
|
+
// Fallback to tabs on error
|
|
51
|
+
router.replace('/(tabs)');
|
|
52
|
+
}<% if (!features.sessionManagement) { %> finally {
|
|
53
|
+
setIsReady(true);
|
|
54
|
+
}<% } %>
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
initializeApp();
|
|
58
|
+
}, [<% if (features.sessionManagement) { %>isSessionChecked, initializeSession, <% } %>onboardingEnabled]);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<ThemeProvider>
|
|
62
|
+
<Stack screenOptions={{ headerShown: false }}>
|
|
63
|
+
<% if (features.onboarding.enabled) { %> <Stack.Screen name="(onboarding)" />
|
|
64
|
+
<% } %><% if (features.authentication.enabled) { %> <Stack.Screen name="(auth)" />
|
|
65
|
+
<% } %><% if (features.paywall) { %> <Stack.Screen name="paywall" options={{ presentation: 'modal' }} />
|
|
66
|
+
<% } %> <Stack.Screen name="(tabs)" />
|
|
67
|
+
<Stack.Screen name="+not-found" />
|
|
68
|
+
</Stack>
|
|
69
|
+
</ThemeProvider>
|
|
70
|
+
);
|
|
71
|
+
}
|