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.
Files changed (274) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +642 -0
  3. package/bin/cli.js +12 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +113 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/config/dependencies.d.ts +82 -0
  9. package/dist/config/dependencies.d.ts.map +1 -0
  10. package/dist/config/dependencies.js +82 -0
  11. package/dist/config/dependencies.js.map +1 -0
  12. package/dist/config/presets.d.ts +3 -0
  13. package/dist/config/presets.d.ts.map +1 -0
  14. package/dist/config/presets.js +174 -0
  15. package/dist/config/presets.js.map +1 -0
  16. package/dist/generators/index.d.ts +40 -0
  17. package/dist/generators/index.d.ts.map +1 -0
  18. package/dist/generators/index.js +130 -0
  19. package/dist/generators/index.js.map +1 -0
  20. package/dist/generators/onboarding.d.ts +8 -0
  21. package/dist/generators/onboarding.d.ts.map +1 -0
  22. package/dist/generators/onboarding.js +141 -0
  23. package/dist/generators/onboarding.js.map +1 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +65 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/prompts/features.d.ts +14 -0
  29. package/dist/prompts/features.d.ts.map +1 -0
  30. package/dist/prompts/features.js +96 -0
  31. package/dist/prompts/features.js.map +1 -0
  32. package/dist/prompts/index.d.ts +3 -0
  33. package/dist/prompts/index.d.ts.map +1 -0
  34. package/dist/prompts/index.js +93 -0
  35. package/dist/prompts/index.js.map +1 -0
  36. package/dist/prompts/onboarding.d.ts +6 -0
  37. package/dist/prompts/onboarding.d.ts.map +1 -0
  38. package/dist/prompts/onboarding.js +37 -0
  39. package/dist/prompts/onboarding.js.map +1 -0
  40. package/dist/prompts/orm.d.ts +3 -0
  41. package/dist/prompts/orm.d.ts.map +1 -0
  42. package/dist/prompts/orm.js +23 -0
  43. package/dist/prompts/orm.js.map +1 -0
  44. package/dist/prompts/packageManager.d.ts +2 -0
  45. package/dist/prompts/packageManager.d.ts.map +1 -0
  46. package/dist/prompts/packageManager.js +18 -0
  47. package/dist/prompts/packageManager.js.map +1 -0
  48. package/dist/prompts/platform.d.ts +3 -0
  49. package/dist/prompts/platform.d.ts.map +1 -0
  50. package/dist/prompts/platform.js +21 -0
  51. package/dist/prompts/platform.js.map +1 -0
  52. package/dist/prompts/preset.d.ts +4 -0
  53. package/dist/prompts/preset.d.ts.map +1 -0
  54. package/dist/prompts/preset.js +165 -0
  55. package/dist/prompts/preset.js.map +1 -0
  56. package/dist/prompts/project.d.ts +2 -0
  57. package/dist/prompts/project.d.ts.map +1 -0
  58. package/dist/prompts/project.js +27 -0
  59. package/dist/prompts/project.js.map +1 -0
  60. package/dist/prompts/sdks.d.ts +2 -0
  61. package/dist/prompts/sdks.d.ts.map +1 -0
  62. package/dist/prompts/sdks.js +46 -0
  63. package/dist/prompts/sdks.js.map +1 -0
  64. package/dist/types/index.d.ts +77 -0
  65. package/dist/types/index.d.ts.map +1 -0
  66. package/dist/types/index.js +25 -0
  67. package/dist/types/index.js.map +1 -0
  68. package/dist/utils/cleanup.d.ts +5 -0
  69. package/dist/utils/cleanup.d.ts.map +1 -0
  70. package/dist/utils/cleanup.js +38 -0
  71. package/dist/utils/cleanup.js.map +1 -0
  72. package/dist/utils/copy.d.ts +10 -0
  73. package/dist/utils/copy.d.ts.map +1 -0
  74. package/dist/utils/copy.js +53 -0
  75. package/dist/utils/copy.js.map +1 -0
  76. package/dist/utils/errors.d.ts +33 -0
  77. package/dist/utils/errors.d.ts.map +1 -0
  78. package/dist/utils/errors.js +136 -0
  79. package/dist/utils/errors.js.map +1 -0
  80. package/dist/utils/git.d.ts +5 -0
  81. package/dist/utils/git.d.ts.map +1 -0
  82. package/dist/utils/git.js +33 -0
  83. package/dist/utils/git.js.map +1 -0
  84. package/dist/utils/logger.d.ts +9 -0
  85. package/dist/utils/logger.d.ts.map +1 -0
  86. package/dist/utils/logger.js +22 -0
  87. package/dist/utils/logger.js.map +1 -0
  88. package/dist/utils/package.d.ts +16 -0
  89. package/dist/utils/package.d.ts.map +1 -0
  90. package/dist/utils/package.js +86 -0
  91. package/dist/utils/package.js.map +1 -0
  92. package/dist/utils/system-validation.d.ts +9 -0
  93. package/dist/utils/system-validation.d.ts.map +1 -0
  94. package/dist/utils/system-validation.js +31 -0
  95. package/dist/utils/system-validation.js.map +1 -0
  96. package/dist/utils/template.d.ts +20 -0
  97. package/dist/utils/template.d.ts.map +1 -0
  98. package/dist/utils/template.js +234 -0
  99. package/dist/utils/template.js.map +1 -0
  100. package/dist/utils/validation.d.ts +8 -0
  101. package/dist/utils/validation.d.ts.map +1 -0
  102. package/dist/utils/validation.js +94 -0
  103. package/dist/utils/validation.js.map +1 -0
  104. package/package.json +96 -0
  105. package/templates/base/backend/.dockerignore.ejs +62 -0
  106. package/templates/base/backend/.env.example.ejs +116 -0
  107. package/templates/base/backend/Dockerfile.ejs +142 -0
  108. package/templates/base/backend/controllers/event-queue/index.ts +20 -0
  109. package/templates/base/backend/controllers/event-queue/workers/user.ts +39 -0
  110. package/templates/base/backend/controllers/rest-api/index.ts +48 -0
  111. package/templates/base/backend/controllers/rest-api/plugins/auth.ts +152 -0
  112. package/templates/base/backend/controllers/rest-api/plugins/config.ts +64 -0
  113. package/templates/base/backend/controllers/rest-api/plugins/error-handler.ts +118 -0
  114. package/templates/base/backend/controllers/rest-api/routes/auth.ts.ejs +180 -0
  115. package/templates/base/backend/controllers/rest-api/routes/device-sessions.ts +197 -0
  116. package/templates/base/backend/controllers/rest-api/routes/oauth-web.ts.ejs +375 -0
  117. package/templates/base/backend/controllers/rest-api/server.ts.ejs +87 -0
  118. package/templates/base/backend/domain/device-session/repository.drizzle.ts +209 -0
  119. package/templates/base/backend/domain/device-session/repository.prisma.ts +248 -0
  120. package/templates/base/backend/domain/device-session/schema.ts +72 -0
  121. package/templates/base/backend/domain/session/repository.drizzle.ts +72 -0
  122. package/templates/base/backend/domain/session/repository.prisma.ts +72 -0
  123. package/templates/base/backend/domain/session/schema.ts +29 -0
  124. package/templates/base/backend/domain/user/repository.drizzle.ts +127 -0
  125. package/templates/base/backend/domain/user/repository.prisma.ts +115 -0
  126. package/templates/base/backend/domain/user/schema.ts +14 -0
  127. package/templates/base/backend/drizzle/schema.drizzle.ts +111 -0
  128. package/templates/base/backend/drizzle.config.drizzle.ts +13 -0
  129. package/templates/base/backend/lib/auth.drizzle.ts.ejs +104 -0
  130. package/templates/base/backend/lib/auth.prisma.ts.ejs +97 -0
  131. package/templates/base/backend/lib/constants.ts.ejs +29 -0
  132. package/templates/base/backend/package.json.ejs +50 -0
  133. package/templates/base/backend/prisma/schema.prisma.ejs +102 -0
  134. package/templates/base/backend/prisma.config.prisma.ts +12 -0
  135. package/templates/base/backend/tsconfig.json +39 -0
  136. package/templates/base/backend/utils/db.drizzle.ts +41 -0
  137. package/templates/base/backend/utils/db.prisma.ts +51 -0
  138. package/templates/base/backend/utils/email.ts.ejs +35 -0
  139. package/templates/base/backend/utils/errors.ts +348 -0
  140. package/templates/base/backend/utils/redis.ts.ejs +279 -0
  141. package/templates/base/mobile/.env.example.ejs +35 -0
  142. package/templates/base/mobile/.gitignore.ejs +167 -0
  143. package/templates/base/mobile/app/+not-found.tsx +85 -0
  144. package/templates/base/mobile/app/_layout.tsx.ejs +71 -0
  145. package/templates/base/mobile/app.json.ejs +88 -0
  146. package/templates/base/mobile/assets/images/adaptive-icon.png +0 -0
  147. package/templates/base/mobile/assets/images/favicon.png +0 -0
  148. package/templates/base/mobile/assets/images/icon.png +0 -0
  149. package/templates/base/mobile/assets/images/onboarding_page_1.png +0 -0
  150. package/templates/base/mobile/assets/images/onboarding_page_2.png +0 -0
  151. package/templates/base/mobile/assets/images/onboarding_page_3.png +0 -0
  152. package/templates/base/mobile/assets/images/paywall_image.png +0 -0
  153. package/templates/base/mobile/assets/images/splash.png +0 -0
  154. package/templates/base/mobile/eas.json.ejs +49 -0
  155. package/templates/base/mobile/metro.config.js +9 -0
  156. package/templates/base/mobile/package.json.ejs +53 -0
  157. package/templates/base/mobile/src/components/ui/Button.tsx +131 -0
  158. package/templates/base/mobile/src/components/ui/Card.tsx +68 -0
  159. package/templates/base/mobile/src/components/ui/IconSymbol.tsx +90 -0
  160. package/templates/base/mobile/src/components/ui/Input.tsx +142 -0
  161. package/templates/base/mobile/src/components/ui/LoadingSpinner.tsx +98 -0
  162. package/templates/base/mobile/src/components/ui/OnboardingLayout.tsx +356 -0
  163. package/templates/base/mobile/src/components/ui/PaywallLayout.tsx +311 -0
  164. package/templates/base/mobile/src/components/ui/Skeleton.tsx +58 -0
  165. package/templates/base/mobile/src/components/ui/index.ts +6 -0
  166. package/templates/base/mobile/src/constants/Theme.ts +163 -0
  167. package/templates/base/mobile/src/context/ThemeContext.tsx +157 -0
  168. package/templates/base/mobile/src/lib/auth-client.ts.ejs +51 -0
  169. package/templates/base/mobile/src/services/api.ts.ejs +71 -0
  170. package/templates/base/mobile/src/services/errorService.ts +179 -0
  171. package/templates/base/mobile/src/services/sdkInitializer.ts.ejs +36 -0
  172. package/templates/base/mobile/src/store/index.ts.ejs +18 -0
  173. package/templates/base/mobile/src/store/ui.store.ts +100 -0
  174. package/templates/base/mobile/src/utils/formatters.ts +105 -0
  175. package/templates/base/mobile/src/utils/logger.ts +73 -0
  176. package/templates/base/mobile/src/utils/responsive.ts +234 -0
  177. package/templates/base/mobile/tsconfig.json +32 -0
  178. package/templates/base/web/.env.example.ejs +26 -0
  179. package/templates/base/web/components.json +22 -0
  180. package/templates/base/web/eslint.config.mjs +18 -0
  181. package/templates/base/web/next.config.ts +7 -0
  182. package/templates/base/web/package.json.ejs +35 -0
  183. package/templates/base/web/postcss.config.mjs +7 -0
  184. package/templates/base/web/public/.gitkeep +0 -0
  185. package/templates/base/web/public/file.svg +1 -0
  186. package/templates/base/web/public/globe.svg +1 -0
  187. package/templates/base/web/public/next.svg +1 -0
  188. package/templates/base/web/public/vercel.svg +1 -0
  189. package/templates/base/web/public/window.svg +1 -0
  190. package/templates/base/web/src/app/favicon.ico +0 -0
  191. package/templates/base/web/src/app/globals.css +152 -0
  192. package/templates/base/web/src/app/layout.tsx.ejs +54 -0
  193. package/templates/base/web/src/app/page.tsx.ejs +92 -0
  194. package/templates/base/web/src/components/auth/auth-hydrator.tsx.ejs +19 -0
  195. package/templates/base/web/src/components/auth/protected-route.tsx.ejs +109 -0
  196. package/templates/base/web/src/components/providers/device-session-setup.tsx.ejs +56 -0
  197. package/templates/base/web/src/components/providers/theme-provider.tsx +17 -0
  198. package/templates/base/web/src/components/theme-toggle.tsx +34 -0
  199. package/templates/base/web/src/components/ui/button.tsx +62 -0
  200. package/templates/base/web/src/components/ui/card.tsx +92 -0
  201. package/templates/base/web/src/components/ui/input.tsx +21 -0
  202. package/templates/base/web/src/components/ui/label.tsx +24 -0
  203. package/templates/base/web/src/components/ui/skeleton.tsx +13 -0
  204. package/templates/base/web/src/components/ui/spinner.tsx +20 -0
  205. package/templates/base/web/src/hooks/use-device-session.ts.ejs +40 -0
  206. package/templates/base/web/src/hooks/use-session.ts.ejs +56 -0
  207. package/templates/base/web/src/lib/auth/actions.ts.ejs +334 -0
  208. package/templates/base/web/src/lib/auth/config.ts.ejs +65 -0
  209. package/templates/base/web/src/lib/auth/cookies.ts.ejs +74 -0
  210. package/templates/base/web/src/lib/auth/index.ts.ejs +40 -0
  211. package/templates/base/web/src/lib/auth/oauth.ts.ejs +72 -0
  212. package/templates/base/web/src/lib/auth/pkce.ts.ejs +48 -0
  213. package/templates/base/web/src/lib/auth/sessions.ts.ejs +135 -0
  214. package/templates/base/web/src/lib/auth/user-agent.ts.ejs +47 -0
  215. package/templates/base/web/src/lib/device/actions.ts.ejs +148 -0
  216. package/templates/base/web/src/lib/device/id.ts.ejs +74 -0
  217. package/templates/base/web/src/lib/utils.ts +6 -0
  218. package/templates/base/web/src/proxy.ts.ejs +66 -0
  219. package/templates/base/web/src/store/auth.store.ts.ejs +89 -0
  220. package/templates/base/web/src/store/deviceSession.store.ts.ejs +141 -0
  221. package/templates/base/web/tsconfig.json +34 -0
  222. package/templates/features/mobile/auth/app/(auth)/_layout.tsx +16 -0
  223. package/templates/features/mobile/auth/app/(auth)/login.tsx +86 -0
  224. package/templates/features/mobile/auth/app/(auth)/register.tsx +86 -0
  225. package/templates/features/mobile/auth/components/auth/LoginForm.tsx.ejs +349 -0
  226. package/templates/features/mobile/auth/components/auth/RegisterForm.tsx.ejs +407 -0
  227. package/templates/features/mobile/auth/components/auth/index.ts +2 -0
  228. package/templates/features/mobile/auth/hooks/index.ts.ejs +1 -0
  229. package/templates/features/mobile/auth/hooks/useAuth.ts.ejs +367 -0
  230. package/templates/features/mobile/auth/services/deviceSession.ts +370 -0
  231. package/templates/features/mobile/auth/store/deviceSession.store.ts +326 -0
  232. package/templates/features/mobile/onboarding/app/(onboarding)/_layout.tsx.ejs +11 -0
  233. package/templates/features/mobile/onboarding/app/(onboarding)/page-1.tsx.ejs +52 -0
  234. package/templates/features/mobile/onboarding/app/(onboarding)/page-2.tsx.ejs +52 -0
  235. package/templates/features/mobile/onboarding/app/(onboarding)/page-3.tsx.ejs +60 -0
  236. package/templates/features/mobile/paywall/app/paywall.tsx +550 -0
  237. package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx +26 -0
  238. package/templates/features/mobile/tabs/app/(tabs)/index.tsx +565 -0
  239. package/templates/features/web/.gitkeep +0 -0
  240. package/templates/features/web/auth/app/(app)/dashboard/dashboard-client.tsx.ejs +166 -0
  241. package/templates/features/web/auth/app/(app)/dashboard/page.tsx.ejs +24 -0
  242. package/templates/features/web/auth/app/(app)/layout.tsx.ejs +43 -0
  243. package/templates/features/web/auth/app/(app)/settings/sessions/page.tsx.ejs +29 -0
  244. package/templates/features/web/auth/app/(app)/settings/sessions/sessions-client.tsx.ejs +77 -0
  245. package/templates/features/web/auth/app/(auth)/forgot-password/page.tsx.ejs +127 -0
  246. package/templates/features/web/auth/app/(auth)/layout.tsx.ejs +32 -0
  247. package/templates/features/web/auth/app/(auth)/login/page.tsx.ejs +35 -0
  248. package/templates/features/web/auth/app/(auth)/register/page.tsx.ejs +19 -0
  249. package/templates/features/web/auth/app/(auth)/reset-password/page.tsx.ejs +40 -0
  250. package/templates/features/web/auth/app/(auth)/verify-email/page.tsx.ejs +198 -0
  251. package/templates/features/web/auth/app/auth/callback/route.ts.ejs +152 -0
  252. package/templates/features/web/auth/components/auth/login-form.tsx.ejs +100 -0
  253. package/templates/features/web/auth/components/auth/oauth-buttons.tsx.ejs +126 -0
  254. package/templates/features/web/auth/components/auth/password-reset-form.tsx.ejs +103 -0
  255. package/templates/features/web/auth/components/auth/register-form.tsx.ejs +139 -0
  256. package/templates/features/web/auth/components/settings/session-card.tsx.ejs +132 -0
  257. package/templates/integrations/mobile/adjust/services/adjustService.ts.ejs +163 -0
  258. package/templates/integrations/mobile/adjust/store/adjust.store.ts +243 -0
  259. package/templates/integrations/mobile/att/services/attService.ts +84 -0
  260. package/templates/integrations/mobile/att/services/trackingPermissions.ts +208 -0
  261. package/templates/integrations/mobile/att/store/att.store.ts +162 -0
  262. package/templates/integrations/mobile/revenuecat/services/revenuecatService.ts.ejs +174 -0
  263. package/templates/integrations/mobile/revenuecat/store/revenuecat.store.ts +286 -0
  264. package/templates/integrations/mobile/scate/services/scateService.ts.ejs +85 -0
  265. package/templates/integrations/mobile/scate/store/scate.store.ts +125 -0
  266. package/templates/integrations/web/.gitkeep +0 -0
  267. package/templates/shared/.env.example.ejs +21 -0
  268. package/templates/shared/.gitignore.ejs +145 -0
  269. package/templates/shared/README.md.ejs +134 -0
  270. package/templates/shared/docker-compose.prod.yml.ejs +120 -0
  271. package/templates/shared/docker-compose.yml.ejs +129 -0
  272. package/templates/shared/scripts/docker-dev.sh.ejs +395 -0
  273. package/templates/shared/scripts/docker-prod.sh.ejs +542 -0
  274. package/templates/shared/scripts/setup.sh.ejs +979 -0
@@ -0,0 +1,139 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import Link from "next/link";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Input } from "@/components/ui/input";
8
+ import { Label } from "@/components/ui/label";
9
+ import { signUp } from "@/lib/auth/actions";
10
+
11
+ export function RegisterForm() {
12
+ const router = useRouter();
13
+ const [name, setName] = useState("");
14
+ const [email, setEmail] = useState("");
15
+ const [password, setPassword] = useState("");
16
+ const [confirmPassword, setConfirmPassword] = useState("");
17
+ const [error, setError] = useState<string | null>(null);
18
+ const [isLoading, setIsLoading] = useState(false);
19
+
20
+ const handleSubmit = async (e: React.FormEvent) => {
21
+ e.preventDefault();
22
+ setError(null);
23
+
24
+ if (password !== confirmPassword) {
25
+ setError("Passwords do not match");
26
+ return;
27
+ }
28
+
29
+ if (password.length < 8) {
30
+ setError("Password must be at least 8 characters");
31
+ return;
32
+ }
33
+
34
+ setIsLoading(true);
35
+
36
+ try {
37
+ const result = await signUp(email, password, name || undefined);
38
+
39
+ if (!result.success) {
40
+ setError(result.error || "Failed to create account");
41
+ return;
42
+ }
43
+
44
+ <% if (features.authentication.emailVerification) { %>
45
+ if (result.needsEmailVerification) {
46
+ router.push(`/verify-email?email=${encodeURIComponent(email)}`);
47
+ } else {
48
+ router.push("/dashboard");
49
+ router.refresh();
50
+ }
51
+ <% } else { %>
52
+ router.push("/dashboard");
53
+ router.refresh();
54
+ <% } %>
55
+ } catch {
56
+ setError("An unexpected error occurred");
57
+ } finally {
58
+ setIsLoading(false);
59
+ }
60
+ };
61
+
62
+ return (
63
+ <form onSubmit={handleSubmit} className="space-y-3">
64
+ <div className="space-y-1.5">
65
+ <Label htmlFor="name" className="text-sm">
66
+ Name <span className="text-muted-foreground font-normal">(optional)</span>
67
+ </Label>
68
+ <Input
69
+ id="name"
70
+ type="text"
71
+ value={name}
72
+ onChange={(e) => setName(e.target.value)}
73
+ placeholder="John Doe"
74
+ disabled={isLoading}
75
+ autoComplete="name"
76
+ />
77
+ </div>
78
+
79
+ <div className="space-y-1.5">
80
+ <Label htmlFor="email" className="text-sm">Email</Label>
81
+ <Input
82
+ id="email"
83
+ type="email"
84
+ value={email}
85
+ onChange={(e) => setEmail(e.target.value)}
86
+ placeholder="you@example.com"
87
+ required
88
+ disabled={isLoading}
89
+ autoComplete="email"
90
+ />
91
+ </div>
92
+
93
+ <div className="space-y-1.5">
94
+ <Label htmlFor="password" className="text-sm">Password</Label>
95
+ <Input
96
+ id="password"
97
+ type="password"
98
+ value={password}
99
+ onChange={(e) => setPassword(e.target.value)}
100
+ placeholder="Min. 8 characters"
101
+ required
102
+ disabled={isLoading}
103
+ autoComplete="new-password"
104
+ />
105
+ </div>
106
+
107
+ <div className="space-y-1.5">
108
+ <Label htmlFor="confirmPassword" className="text-sm">Confirm Password</Label>
109
+ <Input
110
+ id="confirmPassword"
111
+ type="password"
112
+ value={confirmPassword}
113
+ onChange={(e) => setConfirmPassword(e.target.value)}
114
+ placeholder="Confirm your password"
115
+ required
116
+ disabled={isLoading}
117
+ autoComplete="new-password"
118
+ />
119
+ </div>
120
+
121
+ {error && (
122
+ <div className="p-2.5 text-sm text-destructive bg-destructive/10 rounded-lg border border-destructive/20">
123
+ {error}
124
+ </div>
125
+ )}
126
+
127
+ <Button type="submit" className="w-full" disabled={isLoading}>
128
+ {isLoading ? "Creating account..." : "Create account"}
129
+ </Button>
130
+
131
+ <p className="text-center text-sm text-muted-foreground">
132
+ Already have an account?{" "}
133
+ <Link href="/login" className="text-primary hover:underline font-medium">
134
+ Sign in
135
+ </Link>
136
+ </p>
137
+ </form>
138
+ );
139
+ }
@@ -0,0 +1,132 @@
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { type SessionInfo } from "@/lib/auth/sessions";
5
+ import { parseUserAgent } from "@/lib/auth/user-agent";
6
+
7
+ interface SessionCardProps {
8
+ session: SessionInfo;
9
+ isCurrentSession: boolean;
10
+ }
11
+
12
+ export function SessionCard({ session, isCurrentSession }: SessionCardProps) {
13
+ const { browser, os, device } = parseUserAgent(session.userAgent);
14
+ const createdAt = new Date(session.createdAt);
15
+ const expiresAt = new Date(session.expiresAt);
16
+ const isExpired = expiresAt < new Date();
17
+
18
+ const handleRevoke = () => {
19
+ // Session revocation requires WebSocket support for proper real-time notification
20
+ // Without it, the revoked device would experience broken states (401 errors, failed forms)
21
+ alert("Session revocation will be available after WebSocket support is implemented.");
22
+ };
23
+
24
+ return (
25
+ <div
26
+ className={`p-4 border rounded-lg ${
27
+ isCurrentSession
28
+ ? "border-primary bg-primary/5"
29
+ : isExpired
30
+ ? "border-muted bg-muted/20 opacity-60"
31
+ : "border-border"
32
+ }`}
33
+ >
34
+ <div className="flex items-start justify-between gap-4">
35
+ <div className="flex items-start gap-3">
36
+ {/* Device Icon */}
37
+ <div className="mt-1">
38
+ {device === "Mobile" ? (
39
+ <svg
40
+ className="w-5 h-5 text-muted-foreground"
41
+ fill="none"
42
+ viewBox="0 0 24 24"
43
+ stroke="currentColor"
44
+ >
45
+ <path
46
+ strokeLinecap="round"
47
+ strokeLinejoin="round"
48
+ strokeWidth={2}
49
+ d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"
50
+ />
51
+ </svg>
52
+ ) : device === "Tablet" ? (
53
+ <svg
54
+ className="w-5 h-5 text-muted-foreground"
55
+ fill="none"
56
+ viewBox="0 0 24 24"
57
+ stroke="currentColor"
58
+ >
59
+ <path
60
+ strokeLinecap="round"
61
+ strokeLinejoin="round"
62
+ strokeWidth={2}
63
+ d="M12 18h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"
64
+ />
65
+ </svg>
66
+ ) : (
67
+ <svg
68
+ className="w-5 h-5 text-muted-foreground"
69
+ fill="none"
70
+ viewBox="0 0 24 24"
71
+ stroke="currentColor"
72
+ >
73
+ <path
74
+ strokeLinecap="round"
75
+ strokeLinejoin="round"
76
+ strokeWidth={2}
77
+ d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
78
+ />
79
+ </svg>
80
+ )}
81
+ </div>
82
+
83
+ {/* Session Info */}
84
+ <div className="space-y-1">
85
+ <div className="flex items-center gap-2">
86
+ <span className="font-medium">
87
+ {browser} on {os}
88
+ </span>
89
+ {isCurrentSession && (
90
+ <span className="px-2 py-0.5 text-xs font-medium bg-primary text-primary-foreground rounded-full">
91
+ Current
92
+ </span>
93
+ )}
94
+ {isExpired && (
95
+ <span className="px-2 py-0.5 text-xs font-medium bg-muted text-muted-foreground rounded-full">
96
+ Expired
97
+ </span>
98
+ )}
99
+ </div>
100
+
101
+ <div className="text-sm text-muted-foreground space-y-0.5">
102
+ {session.ipAddress && (
103
+ <p>IP: {session.ipAddress}</p>
104
+ )}
105
+ <p>
106
+ Created: {createdAt.toLocaleDateString()} at{" "}
107
+ {createdAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
108
+ </p>
109
+ <p>
110
+ {isExpired ? "Expired" : "Expires"}:{" "}
111
+ {expiresAt.toLocaleDateString()} at{" "}
112
+ {expiresAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
113
+ </p>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ {/* Revoke Button */}
119
+ {!isCurrentSession && !isExpired && (
120
+ <Button
121
+ variant="outline"
122
+ size="sm"
123
+ onClick={handleRevoke}
124
+ className="shrink-0"
125
+ >
126
+ Revoke
127
+ </Button>
128
+ )}
129
+ </div>
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,163 @@
1
+ import { Platform } from 'react-native';
2
+ import { Adjust, AdjustConfig } from 'react-native-adjust';
3
+ import Constants from 'expo-constants';
4
+ import { logger } from '../utils/logger';
5
+
6
+ class AdjustService {
7
+ private static readonly APP_TOKEN = Constants.expoConfig?.extra?.adjust?.appToken || '';
8
+ private static readonly ATT_CONSENT_WAITING_INTERVAL = 120;
9
+
10
+ /**
11
+ * Initialize Adjust SDK
12
+ */
13
+ initialize(): void {
14
+ try {
15
+ logger.info('AdjustService: Initializing Adjust SDK...');
16
+
17
+ // Determine environment from config or fallback to __DEV__
18
+ const configEnvironment = Constants.expoConfig?.extra?.adjust?.environment;
19
+ const environment = configEnvironment === 'production'
20
+ ? AdjustConfig.EnvironmentProduction
21
+ : configEnvironment === 'sandbox'
22
+ ? AdjustConfig.EnvironmentSandbox
23
+ : __DEV__ ? AdjustConfig.EnvironmentSandbox : AdjustConfig.EnvironmentProduction;
24
+
25
+ const adjustConfig = new AdjustConfig(
26
+ AdjustService.APP_TOKEN,
27
+ environment
28
+ );
29
+
30
+ // Set ATT consent waiting interval - gives time for ATT prompt response
31
+ adjustConfig.setAttConsentWaitingInterval(AdjustService.ATT_CONSENT_WAITING_INTERVAL);
32
+
33
+ // Initialize the SDK
34
+ Adjust.initSdk(adjustConfig);
35
+
36
+ logger.info('AdjustService: Adjust SDK initialized successfully');
37
+ } catch (error) {
38
+ logger.error('AdjustService: Failed to initialize Adjust SDK', { error });
39
+ throw error;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Retrieve ADID with retry mechanism and exponential backoff
45
+ */
46
+ async retrieveAdidWithRetry(
47
+ maxAttempts: number = 5,
48
+ maxTotalTimeMs: number = 10000,
49
+ initialDelayMs: number = 500
50
+ ): Promise<string | null> {
51
+ const startTime = Date.now();
52
+ let cumulativeDelay = 0;
53
+
54
+ logger.info('AdjustService: Starting ADID retrieval with exponential backoff retry...');
55
+
56
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
57
+ // Check if we have time for this attempt
58
+ const elapsedTime = Date.now() - startTime;
59
+ const remainingTime = maxTotalTimeMs - elapsedTime - cumulativeDelay;
60
+
61
+ if (remainingTime <= 0) {
62
+ logger.info(`AdjustService: ADID retry timeout reached before attempt ${attempt}`);
63
+ break;
64
+ }
65
+
66
+ // Calculate delay for this attempt (exponential backoff)
67
+ const delay = attempt === 1 ? initialDelayMs : Math.min(
68
+ initialDelayMs * Math.pow(2, attempt - 2),
69
+ remainingTime
70
+ );
71
+
72
+ // Wait before attempting (except for first attempt)
73
+ if (attempt > 1) {
74
+ logger.info(`AdjustService: ADID retry attempt ${attempt}/${maxAttempts} - waiting ${delay}ms...`);
75
+ await new Promise(resolve => setTimeout(resolve, delay));
76
+ cumulativeDelay += delay;
77
+ } else {
78
+ logger.info(`AdjustService: ADID attempt ${attempt}/${maxAttempts} (initial attempt)...`);
79
+ }
80
+
81
+ // Check remaining time after delay
82
+ const currentElapsed = Date.now() - startTime;
83
+ if (currentElapsed >= maxTotalTimeMs) {
84
+ logger.info('AdjustService: ADID retry timeout reached after waiting');
85
+ break;
86
+ }
87
+
88
+ // Attempt to retrieve ADID
89
+ try {
90
+ const result = await new Promise<string | null>((resolve) => {
91
+ Adjust.getAdid((retrievedAdid) => {
92
+ logger.info(`AdjustService: ADID attempt ${attempt} result:`, retrievedAdid);
93
+ resolve(retrievedAdid);
94
+ });
95
+ });
96
+
97
+ if (result) {
98
+ const totalTime = Date.now() - startTime;
99
+ logger.info(`AdjustService: ADID retrieved successfully on attempt ${attempt} after ${totalTime}ms:`, result);
100
+ return result;
101
+ } else {
102
+ logger.info(`AdjustService: ADID attempt ${attempt} returned null`);
103
+ }
104
+ } catch (error) {
105
+ logger.error(`AdjustService: ADID attempt ${attempt} failed:`, error);
106
+ }
107
+ }
108
+
109
+ const totalTime = Date.now() - startTime;
110
+ logger.info(`AdjustService: ADID retrieval failed after ${maxAttempts} attempts in ${totalTime}ms`);
111
+ return null;
112
+ }
113
+
114
+ /**
115
+ * Get ADID immediately (single attempt)
116
+ */
117
+ async getAdid(): Promise<string | null> {
118
+ try {
119
+ return new Promise<string | null>((resolve) => {
120
+ Adjust.getAdid((adid) => {
121
+ resolve(adid);
122
+ });
123
+ });
124
+ } catch (error) {
125
+ logger.error('AdjustService: Failed to get ADID', { error });
126
+ return null;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Track event with Adjust
132
+ */
133
+ trackEvent(eventToken: string, revenue?: number, currency?: string): void {
134
+ try {
135
+ // Implementation would depend on your event tracking needs
136
+ logger.info('AdjustService: Event tracking would be implemented here', {
137
+ eventToken,
138
+ revenue,
139
+ currency
140
+ });
141
+ } catch (error) {
142
+ logger.error('AdjustService: Failed to track event', { error });
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Get environment info
148
+ */
149
+ getEnvironment(): string {
150
+ const configEnvironment = Constants.expoConfig?.extra?.adjust?.environment;
151
+ return configEnvironment || (__DEV__ ? 'sandbox' : 'production');
152
+ }
153
+
154
+ /**
155
+ * Get app token
156
+ */
157
+ getAppToken(): string {
158
+ return AdjustService.APP_TOKEN;
159
+ }
160
+ }
161
+
162
+ // Export singleton instance
163
+ export const adjustService = new AdjustService();
@@ -0,0 +1,243 @@
1
+ import { create } from 'zustand';
2
+ import { adjustService } from '../services/adjustService';
3
+ import { useRevenueCatStore } from './revenuecat.store';
4
+ import { useScateStore } from './scate.store';
5
+ import { logger } from '../utils/logger';
6
+
7
+ interface AdjustState {
8
+ // State
9
+ isInitialized: boolean;
10
+ adid: string | null;
11
+ isRetrievingAdid: boolean;
12
+ isLoading: boolean;
13
+ error: string | null;
14
+
15
+ // Actions
16
+ initialize: () => void;
17
+ retrieveAdid: () => Promise<void>;
18
+ retrieveAdidWithRetry: () => Promise<void>;
19
+ setAdid: (adid: string | null) => void;
20
+ setLoading: (loading: boolean) => void;
21
+ setError: (error: string | null) => void;
22
+ reset: () => void;
23
+ notifyOtherStores: (adid: string) => void;
24
+
25
+ // Computed values
26
+ hasAdid: boolean;
27
+ }
28
+
29
+ export const useAdjustStore = create<AdjustState>((set, get) => ({
30
+ // Initial state
31
+ isInitialized: false,
32
+ adid: null,
33
+ isRetrievingAdid: false,
34
+ isLoading: false,
35
+ error: null,
36
+ hasAdid: false,
37
+
38
+ // Actions
39
+ initialize: () => {
40
+ const state = get();
41
+ if (state.isInitialized) {
42
+ logger.info('AdjustStore: Already initialized, skipping');
43
+ return;
44
+ }
45
+
46
+ try {
47
+ set({ isLoading: true, error: null });
48
+ logger.info('AdjustStore: Initializing Adjust SDK...');
49
+
50
+ adjustService.initialize();
51
+
52
+ set({
53
+ isInitialized: true,
54
+ isLoading: false,
55
+ error: null,
56
+ hasAdid: false
57
+ });
58
+
59
+ logger.info('AdjustStore: Adjust SDK initialized successfully');
60
+ } catch (error) {
61
+ const errorMessage = error instanceof Error ? error.message : 'Failed to initialize Adjust SDK';
62
+ logger.error('AdjustStore: Failed to initialize Adjust SDK', { error });
63
+ set({
64
+ error: errorMessage,
65
+ isLoading: false,
66
+ isInitialized: false,
67
+ hasAdid: false
68
+ });
69
+ }
70
+ },
71
+
72
+ retrieveAdid: async () => {
73
+ const state = get();
74
+ if (state.isRetrievingAdid || state.adid) {
75
+ logger.info('AdjustStore: ADID retrieval already in progress or ADID already available');
76
+ return;
77
+ }
78
+
79
+ try {
80
+ set({ isRetrievingAdid: true, error: null });
81
+ logger.info('AdjustStore: Retrieving ADID...');
82
+
83
+ const adid = await adjustService.getAdid();
84
+
85
+ if (adid) {
86
+ set({
87
+ adid,
88
+ isRetrievingAdid: false,
89
+ error: null,
90
+ hasAdid: true
91
+ });
92
+
93
+ // Notify other stores about ADID availability
94
+ get().notifyOtherStores(adid);
95
+
96
+ logger.info('AdjustStore: ADID retrieved successfully', { adid });
97
+ } else {
98
+ set({
99
+ adid: null,
100
+ isRetrievingAdid: false,
101
+ error: 'ADID not available',
102
+ hasAdid: false
103
+ });
104
+ logger.info('AdjustStore: ADID not available');
105
+ }
106
+ } catch (error) {
107
+ const errorMessage = error instanceof Error ? error.message : 'Failed to retrieve ADID';
108
+ logger.error('AdjustStore: Failed to retrieve ADID', { error });
109
+ set({
110
+ error: errorMessage,
111
+ isRetrievingAdid: false,
112
+ adid: null,
113
+ hasAdid: false
114
+ });
115
+ }
116
+ },
117
+
118
+ retrieveAdidWithRetry: async () => {
119
+ const state = get();
120
+ if (state.isRetrievingAdid || state.adid) {
121
+ logger.info('AdjustStore: ADID retrieval with retry already in progress or ADID already available');
122
+ return;
123
+ }
124
+
125
+ try {
126
+ set({ isRetrievingAdid: true, error: null });
127
+ logger.info('AdjustStore: Retrieving ADID with retry mechanism...');
128
+
129
+ const adid = await adjustService.retrieveAdidWithRetry();
130
+
131
+ if (adid) {
132
+ set({
133
+ adid,
134
+ isRetrievingAdid: false,
135
+ error: null,
136
+ hasAdid: true
137
+ });
138
+
139
+ // Notify other stores about ADID availability
140
+ get().notifyOtherStores(adid);
141
+
142
+ logger.info('AdjustStore: ADID retrieved successfully with retry', { adid });
143
+ } else {
144
+ set({
145
+ adid: null,
146
+ isRetrievingAdid: false,
147
+ error: 'ADID not available after retry attempts',
148
+ hasAdid: false
149
+ });
150
+ logger.info('AdjustStore: ADID not available after retry attempts');
151
+ }
152
+ } catch (error) {
153
+ const errorMessage = error instanceof Error ? error.message : 'Failed to retrieve ADID with retry';
154
+ logger.error('AdjustStore: Failed to retrieve ADID with retry', { error });
155
+ set({
156
+ error: errorMessage,
157
+ isRetrievingAdid: false,
158
+ adid: null,
159
+ hasAdid: false
160
+ });
161
+ }
162
+ },
163
+
164
+ setAdid: (adid) => {
165
+ const currentState = get();
166
+ if (currentState.adid === adid) {
167
+ return; // No change needed
168
+ }
169
+
170
+ set({ adid, hasAdid: !!adid });
171
+
172
+ if (adid) {
173
+ // Notify other stores about ADID availability
174
+ get().notifyOtherStores(adid);
175
+ }
176
+
177
+ logger.info('AdjustStore: ADID set', { adid });
178
+ },
179
+
180
+ setLoading: (isLoading) => set({ isLoading }),
181
+
182
+ setError: (error) => set({ error }),
183
+
184
+ reset: () => {
185
+ set({
186
+ isInitialized: false,
187
+ adid: null,
188
+ isRetrievingAdid: false,
189
+ isLoading: false,
190
+ error: null,
191
+ hasAdid: false
192
+ });
193
+ logger.info('AdjustStore: Store reset');
194
+ },
195
+
196
+ // Helper method to notify other stores about ADID
197
+ notifyOtherStores: (adid: string) => {
198
+ try {
199
+ // Notify RevenueCat store
200
+ const revenueCatStore = useRevenueCatStore.getState();
201
+ if (revenueCatStore.isInitialized) {
202
+ revenueCatStore.setAdjustId(adid);
203
+ }
204
+
205
+ // Notify Scate store
206
+ const scateStore = useScateStore.getState();
207
+ if (scateStore.isInitialized) {
208
+ scateStore.setAdid(adid);
209
+ }
210
+
211
+ logger.info('AdjustStore: Other stores notified about ADID', { adid });
212
+ } catch (error) {
213
+ logger.error('AdjustStore: Failed to notify other stores about ADID', { error });
214
+ }
215
+ },
216
+
217
+ }));
218
+
219
+ // Selectors for commonly used Adjust state
220
+ export const useAdjust = () => {
221
+ const state = useAdjustStore();
222
+ return {
223
+ isInitialized: state.isInitialized,
224
+ adid: state.adid,
225
+ isRetrievingAdid: state.isRetrievingAdid,
226
+ isLoading: state.isLoading,
227
+ error: state.error,
228
+ hasAdid: state.hasAdid,
229
+ };
230
+ };
231
+
232
+ export const useAdjustActions = () => {
233
+ const state = useAdjustStore();
234
+ return {
235
+ initialize: state.initialize,
236
+ retrieveAdid: state.retrieveAdid,
237
+ retrieveAdidWithRetry: state.retrieveAdidWithRetry,
238
+ setAdid: state.setAdid,
239
+ setLoading: state.setLoading,
240
+ setError: state.setError,
241
+ reset: state.reset,
242
+ };
243
+ };