create-stackr 0.2.0 → 0.3.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/README.md +10 -0
- package/dist/prompts/features.d.ts +1 -1
- package/dist/prompts/features.d.ts.map +1 -1
- package/dist/prompts/features.js +34 -25
- package/dist/prompts/features.js.map +1 -1
- package/dist/prompts/index.js +33 -6
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/preset.d.ts.map +1 -1
- package/dist/prompts/preset.js +69 -34
- package/dist/prompts/preset.js.map +1 -1
- package/dist/utils/template.js +1 -1
- package/dist/utils/template.js.map +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +43 -1
- package/dist/utils/validation.js.map +1 -1
- package/package.json +1 -1
- package/templates/base/backend/controllers/rest-api/routes/auth.ts.ejs +33 -1
- package/templates/base/backend/controllers/rest-api/routes/oauth-web.ts.ejs +6 -0
- package/templates/base/backend/domain/user/repository.drizzle.ts +28 -1
- package/templates/base/backend/domain/user/repository.prisma.ts +25 -0
- package/templates/base/backend/drizzle/{schema.drizzle.ts → schema.drizzle.ts.ejs} +25 -0
- package/templates/base/backend/lib/auth.drizzle.ts.ejs +27 -18
- package/templates/base/backend/lib/auth.prisma.ts.ejs +24 -18
- package/templates/base/backend/package.json.ejs +29 -23
- package/templates/base/backend/prisma/schema.prisma.ejs +20 -0
- package/templates/base/mobile/app/+not-found.tsx +1 -1
- package/templates/base/mobile/app/_layout.tsx.ejs +95 -10
- package/templates/base/mobile/package.json.ejs +21 -13
- package/templates/base/mobile/src/components/ui/Button.tsx +5 -3
- package/templates/base/mobile/src/components/ui/Card.tsx +1 -1
- package/templates/base/mobile/src/components/ui/Input.tsx +1 -1
- package/templates/base/mobile/src/components/ui/Skeleton.tsx +1 -1
- package/templates/base/mobile/src/components/ui/Toast.tsx +106 -0
- package/templates/base/mobile/src/components/ui/{IconSymbol.tsx → icon-symbol.tsx} +7 -0
- package/templates/base/mobile/src/components/ui/{LoadingSpinner.tsx → loading-spinner.tsx} +1 -1
- package/templates/base/mobile/src/components/ui/{OnboardingLayout.tsx → onboarding-layout.tsx} +2 -2
- package/templates/base/mobile/src/components/ui/{PaywallLayout.tsx → paywall-layout.tsx} +3 -3
- package/templates/base/mobile/src/constants/Theme.ts +3 -3
- package/templates/base/mobile/src/context/{ThemeContext.tsx → theme-context.tsx} +2 -2
- package/templates/base/mobile/src/lib/auth-client.ts.ejs +18 -0
- package/templates/base/mobile/src/services/{sdkInitializer.ts.ejs → sdk-initializer.ts.ejs} +4 -4
- package/templates/base/web/.prettierignore +6 -0
- package/templates/base/web/.prettierrc +8 -0
- package/templates/base/web/eslint.config.mjs +31 -7
- package/templates/base/web/next.config.ts +50 -1
- package/templates/base/web/package.json.ejs +14 -2
- package/templates/base/web/src/app/globals.css +1 -1
- package/templates/base/web/src/app/layout.tsx.ejs +2 -0
- package/templates/base/web/src/components/auth/protected-route.tsx.ejs +32 -5
- package/templates/base/web/src/components/providers/device-session-setup.tsx.ejs +1 -1
- package/templates/base/web/src/hooks/use-device-session.ts.ejs +2 -2
- package/templates/base/web/src/lib/auth/actions.ts.ejs +438 -15
- package/templates/base/web/src/lib/auth/config.ts.ejs +13 -0
- package/templates/base/web/src/lib/auth/cookies.ts.ejs +61 -0
- package/templates/base/web/src/lib/device/actions.ts.ejs +2 -10
- package/templates/base/web/src/lib/device/types.ts +37 -0
- package/templates/base/web/src/proxy.ts.ejs +12 -2
- package/templates/base/web/src/store/{deviceSession.store.ts.ejs → device-session-store.ts.ejs} +1 -1
- package/templates/features/mobile/auth/app/(auth)/{_layout.tsx → _layout.tsx.ejs} +7 -0
- package/templates/features/mobile/auth/app/(auth)/{login.tsx → login.tsx.ejs} +9 -8
- package/templates/features/mobile/auth/app/(auth)/{register.tsx → register.tsx.ejs} +9 -8
- package/templates/features/mobile/auth/app/(auth)/two-factor-expired.tsx.ejs +88 -0
- package/templates/features/mobile/auth/app/(auth)/two-factor.tsx.ejs +259 -0
- package/templates/features/mobile/auth/app/(public)/_layout.tsx +9 -0
- package/templates/features/mobile/{tabs/app/(tabs)/index.tsx → auth/app/(public)/index.tsx.ejs} +76 -257
- package/templates/features/mobile/auth/app/(tabs)/settings/_layout.tsx.ejs +20 -0
- package/templates/features/mobile/auth/app/(tabs)/settings/index.tsx.ejs +137 -0
- package/templates/features/mobile/auth/app/(tabs)/settings/security/_layout.tsx.ejs +35 -0
- package/templates/features/mobile/auth/app/(tabs)/settings/security/index.tsx.ejs +147 -0
- package/templates/features/mobile/auth/app/(tabs)/settings/security/set-password.tsx.ejs +140 -0
- package/templates/features/mobile/auth/app/(tabs)/settings/security/two-factor-manage.tsx.ejs +366 -0
- package/templates/features/mobile/auth/app/(tabs)/settings/security/two-factor-setup.tsx.ejs +317 -0
- package/templates/features/mobile/auth/app/account.tsx.ejs +331 -0
- package/templates/features/mobile/auth/app/verify-email.tsx.ejs +315 -0
- package/templates/features/mobile/auth/components/auth/{LoginForm.tsx.ejs → login-form.tsx.ejs} +15 -3
- package/templates/features/mobile/auth/components/auth/protected-screen.tsx.ejs +56 -0
- package/templates/features/mobile/auth/components/auth/{RegisterForm.tsx.ejs → register-form.tsx.ejs} +5 -3
- package/templates/features/mobile/auth/hooks/{useAuth.ts.ejs → auth.ts.ejs} +255 -1
- package/templates/features/mobile/auth/hooks/two-factor-timeout.ts.ejs +56 -0
- package/templates/features/mobile/auth/services/{deviceSession.ts → device-session.ts} +2 -10
- package/templates/features/mobile/auth/store/{deviceSession.store.ts → device-session-store.ts} +14 -5
- package/templates/features/mobile/auth/types/device-session.ts +37 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-1.tsx.ejs +3 -1
- package/templates/features/mobile/onboarding/app/(onboarding)/page-2.tsx.ejs +3 -1
- package/templates/features/mobile/onboarding/app/(onboarding)/page-3.tsx.ejs +6 -1
- package/templates/features/mobile/paywall/app/paywall.tsx +4 -4
- package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx +0 -6
- package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx.ejs +60 -0
- package/templates/features/mobile/tabs/app/(tabs)/index.tsx.ejs +264 -0
- package/templates/features/web/auth/app/(app)/layout.tsx.ejs +8 -22
- package/templates/features/web/auth/app/(auth)/login/page.tsx.ejs +19 -3
- package/templates/features/web/auth/app/(auth)/login/two-factor/page.tsx.ejs +24 -0
- package/templates/features/web/auth/app/(auth)/verify-email/page.tsx.ejs +117 -152
- package/templates/features/web/auth/app/{(app) → (protected)}/dashboard/dashboard-client.tsx.ejs +41 -1
- package/templates/features/web/auth/app/{(app) → (protected)}/dashboard/page.tsx.ejs +3 -1
- package/templates/features/web/auth/app/(protected)/layout.tsx.ejs +67 -0
- package/templates/features/web/auth/app/(protected)/settings/security/page.tsx.ejs +19 -0
- package/templates/features/web/auth/app/(protected)/settings/security/security-settings.tsx.ejs +73 -0
- package/templates/features/web/auth/app/{(app) → (protected)}/settings/sessions/page.tsx.ejs +2 -6
- package/templates/features/web/auth/app/auth/callback/route.ts.ejs +5 -1
- package/templates/features/web/auth/app/auth/session-expired/route.ts.ejs +39 -0
- package/templates/features/web/auth/app/auth/two-factor-expired/route.ts.ejs +22 -0
- package/templates/features/web/auth/components/auth/login-form.tsx.ejs +18 -0
- package/templates/features/web/auth/components/auth/two-factor-verify.tsx.ejs +173 -0
- package/templates/features/web/auth/components/settings/set-password-form.tsx.ejs +123 -0
- package/templates/features/web/auth/components/settings/two-factor-manage.tsx.ejs +253 -0
- package/templates/features/web/auth/components/settings/two-factor-setup.tsx.ejs +249 -0
- package/templates/features/web/auth/components/ui/checkbox.tsx +32 -0
- package/templates/features/web/auth/components/ui/dialog.tsx +143 -0
- package/templates/features/web/auth/components/ui/input-otp.tsx +71 -0
- package/templates/integrations/mobile/adjust/store/{adjust.store.ts → adjust-store.ts} +3 -3
- package/templates/integrations/mobile/att/store/{att.store.ts → att-store.ts} +2 -2
- package/templates/integrations/mobile/revenuecat/store/{revenuecat.store.ts → revenuecat-store.ts} +1 -1
- package/templates/integrations/mobile/scate/store/{scate.store.ts → scate-store.ts} +1 -1
- package/templates/base/mobile/src/components/ui/index.ts +0 -6
- package/templates/base/mobile/src/store/index.ts.ejs +0 -18
- package/templates/base/web/src/lib/auth/index.ts.ejs +0 -40
- package/templates/features/mobile/auth/components/auth/index.ts +0 -2
- package/templates/features/mobile/auth/hooks/index.ts.ejs +0 -1
- /package/templates/base/mobile/src/services/{errorService.ts → error-service.ts} +0 -0
- /package/templates/base/mobile/src/store/{ui.store.ts → ui-store.ts} +0 -0
- /package/templates/features/web/auth/app/{(app) → (protected)}/settings/sessions/sessions-client.tsx.ejs +0 -0
- /package/templates/integrations/mobile/adjust/services/{adjustService.ts.ejs → adjust-service.ts.ejs} +0 -0
- /package/templates/integrations/mobile/att/services/{attService.ts → att-service.ts} +0 -0
- /package/templates/integrations/mobile/att/services/{trackingPermissions.ts → tracking-permissions.ts} +0 -0
- /package/templates/integrations/mobile/revenuecat/services/{revenuecatService.ts.ejs → revenuecat-service.ts.ejs} +0 -0
- /package/templates/integrations/mobile/scate/services/{scateService.ts.ejs → scate-service.ts.ejs} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eq } from 'drizzle-orm';
|
|
1
|
+
import { eq, and } from 'drizzle-orm';
|
|
2
2
|
import { db, schema } from "../../utils/db";
|
|
3
3
|
import { ErrorFactory } from "../../utils/errors";
|
|
4
4
|
|
|
@@ -125,3 +125,30 @@ export const deleteUser = async (userId: string): Promise<void> => {
|
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
127
|
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if user has a credential account with password
|
|
131
|
+
* Returns true if user signed up with email/password, false for OAuth-only users
|
|
132
|
+
*/
|
|
133
|
+
export const userHasPassword = async (userId: string): Promise<boolean> => {
|
|
134
|
+
try {
|
|
135
|
+
const [credentialAccount] = await db
|
|
136
|
+
.select({ password: schema.account.password })
|
|
137
|
+
.from(schema.account)
|
|
138
|
+
.where(
|
|
139
|
+
and(
|
|
140
|
+
eq(schema.account.userId, userId),
|
|
141
|
+
eq(schema.account.providerId, 'credential')
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
.limit(1);
|
|
145
|
+
|
|
146
|
+
return !!credentialAccount?.password;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw ErrorFactory.databaseError({
|
|
149
|
+
operation: 'userHasPassword',
|
|
150
|
+
userId,
|
|
151
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
};
|
|
@@ -112,4 +112,29 @@ export const deleteUser = async (userId: string): Promise<void> => {
|
|
|
112
112
|
originalError: error instanceof Error ? error.message : String(error)
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if user has a credential account with password
|
|
119
|
+
* Returns true if user signed up with email/password, false for OAuth-only users
|
|
120
|
+
*/
|
|
121
|
+
export const userHasPassword = async (userId: string): Promise<boolean> => {
|
|
122
|
+
try {
|
|
123
|
+
const credentialAccount = await db.account.findFirst({
|
|
124
|
+
where: {
|
|
125
|
+
userId,
|
|
126
|
+
providerId: 'credential',
|
|
127
|
+
password: { not: null },
|
|
128
|
+
},
|
|
129
|
+
select: { password: true },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return !!credentialAccount?.password;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
throw ErrorFactory.databaseError({
|
|
135
|
+
operation: 'userHasPassword',
|
|
136
|
+
userId,
|
|
137
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
138
|
+
});
|
|
139
|
+
}
|
|
115
140
|
};
|
|
@@ -24,6 +24,9 @@ export const user = pgTable('user', {
|
|
|
24
24
|
image: text('image'),
|
|
25
25
|
createdAt: timestamp('created_at', { mode: 'date' }).defaultNow().notNull(),
|
|
26
26
|
updatedAt: timestamp('updated_at', { mode: 'date' }).defaultNow().notNull().$onUpdate(() => new Date()),
|
|
27
|
+
<% if (features.authentication.twoFactor) { %>
|
|
28
|
+
twoFactorEnabled: boolean('two_factor_enabled').default(false),
|
|
29
|
+
<% } %>
|
|
27
30
|
});
|
|
28
31
|
|
|
29
32
|
// BetterAuth Session model
|
|
@@ -67,6 +70,16 @@ export const verification = pgTable('verification', {
|
|
|
67
70
|
updatedAt: timestamp('updated_at', { mode: 'date' }).defaultNow().notNull().$onUpdate(() => new Date()),
|
|
68
71
|
});
|
|
69
72
|
|
|
73
|
+
<% if (features.authentication.twoFactor) { %>
|
|
74
|
+
// BetterAuth TwoFactor model
|
|
75
|
+
export const twoFactor = pgTable('two_factor', {
|
|
76
|
+
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
77
|
+
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
|
|
78
|
+
secret: text('secret'),
|
|
79
|
+
backupCodes: text('backup_codes'),
|
|
80
|
+
});
|
|
81
|
+
<% } %>
|
|
82
|
+
|
|
70
83
|
// ==================== App-Specific Tables ====================
|
|
71
84
|
|
|
72
85
|
// Device Session model (anonymous sessions before authentication)
|
|
@@ -87,6 +100,9 @@ export const userRelations = relations(user, ({ many }) => ({
|
|
|
87
100
|
sessions: many(session),
|
|
88
101
|
accounts: many(account),
|
|
89
102
|
deviceSessions: many(deviceSession),
|
|
103
|
+
<% if (features.authentication.twoFactor) { %>
|
|
104
|
+
twoFactor: many(twoFactor),
|
|
105
|
+
<% } %>
|
|
90
106
|
}));
|
|
91
107
|
|
|
92
108
|
export const sessionRelations = relations(session, ({ one }) => ({
|
|
@@ -103,6 +119,15 @@ export const accountRelations = relations(account, ({ one }) => ({
|
|
|
103
119
|
}),
|
|
104
120
|
}));
|
|
105
121
|
|
|
122
|
+
<% if (features.authentication.twoFactor) { %>
|
|
123
|
+
export const twoFactorRelations = relations(twoFactor, ({ one }) => ({
|
|
124
|
+
user: one(user, {
|
|
125
|
+
fields: [twoFactor.userId],
|
|
126
|
+
references: [user.id],
|
|
127
|
+
}),
|
|
128
|
+
}));
|
|
129
|
+
<% } %>
|
|
130
|
+
|
|
106
131
|
export const deviceSessionRelations = relations(deviceSession, ({ one }) => ({
|
|
107
132
|
migratedToUser: one(user, {
|
|
108
133
|
fields: [deviceSession.migratedToUserId],
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { betterAuth } from "better-auth";
|
|
2
2
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
3
3
|
import { expo } from "@better-auth/expo";
|
|
4
|
-
<% if (features.authentication.twoFactor) { %>
|
|
5
|
-
import { twoFactor } from "better-auth/plugins";
|
|
4
|
+
<% if (features.authentication.twoFactor || features.authentication.emailVerification) { %>
|
|
5
|
+
import { <% if (features.authentication.twoFactor) { %>twoFactor, <% } %><% if (features.authentication.emailVerification) { %>emailOTP<% } %> } from "better-auth/plugins";
|
|
6
6
|
<% } %>
|
|
7
7
|
import { db } from "../utils/db";
|
|
8
8
|
import * as schema from "../drizzle/schema";
|
|
@@ -18,12 +18,15 @@ export const auth = betterAuth({
|
|
|
18
18
|
session: schema.session,
|
|
19
19
|
account: schema.account,
|
|
20
20
|
verification: schema.verification,
|
|
21
|
+
<% if (features.authentication.twoFactor) { %>
|
|
22
|
+
twoFactor: schema.twoFactor,
|
|
23
|
+
<% } %>
|
|
21
24
|
},
|
|
22
25
|
}),
|
|
23
26
|
basePath: "/api/auth", // BetterAuth endpoints will be at /api/auth/*
|
|
24
27
|
emailAndPassword: {
|
|
25
28
|
enabled: true,
|
|
26
|
-
requireEmailVerification:
|
|
29
|
+
requireEmailVerification: false, // Allow sign-in, control access on frontend via ProtectedRoute
|
|
27
30
|
<% if (features.authentication.passwordReset) { %>
|
|
28
31
|
sendResetPassword: async ({ user, url }) => {
|
|
29
32
|
await sendEmail({
|
|
@@ -39,21 +42,6 @@ export const auth = betterAuth({
|
|
|
39
42
|
},
|
|
40
43
|
<% } %>
|
|
41
44
|
},
|
|
42
|
-
<% if (features.authentication.emailVerification) { %>
|
|
43
|
-
emailVerification: {
|
|
44
|
-
sendVerificationEmail: async ({ user, url }) => {
|
|
45
|
-
await sendEmail({
|
|
46
|
-
to: user.email,
|
|
47
|
-
subject: "Verify your email address",
|
|
48
|
-
html: `
|
|
49
|
-
<h1>Verify your email</h1>
|
|
50
|
-
<p>Click the link below to verify your email address:</p>
|
|
51
|
-
<a href="${url}">Verify Email</a>
|
|
52
|
-
`,
|
|
53
|
-
});
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
<% } %>
|
|
57
45
|
<% if (features.authentication.providers.google || features.authentication.providers.apple || features.authentication.providers.github) { %>
|
|
58
46
|
socialProviders: {
|
|
59
47
|
<% if (features.authentication.providers.google) { %>
|
|
@@ -81,6 +69,27 @@ export const auth = betterAuth({
|
|
|
81
69
|
expo(), // Required for React Native/Expo OAuth deep link handling
|
|
82
70
|
<% if (features.authentication.twoFactor) { %>
|
|
83
71
|
twoFactor(),
|
|
72
|
+
<% } %>
|
|
73
|
+
<% if (features.authentication.emailVerification) { %>
|
|
74
|
+
emailOTP({
|
|
75
|
+
otpLength: 6,
|
|
76
|
+
expiresIn: 300, // 5 minutes
|
|
77
|
+
sendVerificationOnSignUp: true,
|
|
78
|
+
async sendVerificationOTP({ email, otp, type }) {
|
|
79
|
+
if (type === 'email-verification') {
|
|
80
|
+
await sendEmail({
|
|
81
|
+
to: email,
|
|
82
|
+
subject: "Your verification code",
|
|
83
|
+
html: `
|
|
84
|
+
<h1>Verify your email</h1>
|
|
85
|
+
<p>Your verification code is:</p>
|
|
86
|
+
<h2 style="font-size: 32px; letter-spacing: 8px; font-family: monospace;">${otp}</h2>
|
|
87
|
+
<p>This code expires in 5 minutes.</p>
|
|
88
|
+
`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
84
93
|
<% } %>
|
|
85
94
|
],
|
|
86
95
|
session: {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { betterAuth } from "better-auth";
|
|
2
2
|
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
3
3
|
import { expo } from "@better-auth/expo";
|
|
4
|
-
<% if (features.authentication.twoFactor) { %>
|
|
5
|
-
import { twoFactor } from "better-auth/plugins";
|
|
4
|
+
<% if (features.authentication.twoFactor || features.authentication.emailVerification) { %>
|
|
5
|
+
import { <% if (features.authentication.twoFactor) { %>twoFactor, <% } %><% if (features.authentication.emailVerification) { %>emailOTP<% } %> } from "better-auth/plugins";
|
|
6
6
|
<% } %>
|
|
7
7
|
import { db } from "../utils/db";
|
|
8
8
|
<% if (features.authentication.emailVerification || features.authentication.passwordReset) { %>
|
|
@@ -16,7 +16,7 @@ export const auth = betterAuth({
|
|
|
16
16
|
basePath: "/api/auth", // BetterAuth endpoints will be at /api/auth/*
|
|
17
17
|
emailAndPassword: {
|
|
18
18
|
enabled: true,
|
|
19
|
-
requireEmailVerification:
|
|
19
|
+
requireEmailVerification: false, // Allow sign-in, control access on frontend via ProtectedRoute
|
|
20
20
|
<% if (features.authentication.passwordReset) { %>
|
|
21
21
|
sendResetPassword: async ({ user, url }) => {
|
|
22
22
|
await sendEmail({
|
|
@@ -32,21 +32,6 @@ export const auth = betterAuth({
|
|
|
32
32
|
},
|
|
33
33
|
<% } %>
|
|
34
34
|
},
|
|
35
|
-
<% if (features.authentication.emailVerification) { %>
|
|
36
|
-
emailVerification: {
|
|
37
|
-
sendVerificationEmail: async ({ user, url }) => {
|
|
38
|
-
await sendEmail({
|
|
39
|
-
to: user.email,
|
|
40
|
-
subject: "Verify your email address",
|
|
41
|
-
html: `
|
|
42
|
-
<h1>Verify your email</h1>
|
|
43
|
-
<p>Click the link below to verify your email address:</p>
|
|
44
|
-
<a href="${url}">Verify Email</a>
|
|
45
|
-
`,
|
|
46
|
-
});
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
<% } %>
|
|
50
35
|
<% if (features.authentication.providers.google || features.authentication.providers.apple || features.authentication.providers.github) { %>
|
|
51
36
|
socialProviders: {
|
|
52
37
|
<% if (features.authentication.providers.google) { %>
|
|
@@ -74,6 +59,27 @@ export const auth = betterAuth({
|
|
|
74
59
|
expo(), // Required for React Native/Expo OAuth deep link handling
|
|
75
60
|
<% if (features.authentication.twoFactor) { %>
|
|
76
61
|
twoFactor(),
|
|
62
|
+
<% } %>
|
|
63
|
+
<% if (features.authentication.emailVerification) { %>
|
|
64
|
+
emailOTP({
|
|
65
|
+
otpLength: 6,
|
|
66
|
+
expiresIn: 300, // 5 minutes
|
|
67
|
+
sendVerificationOnSignUp: true,
|
|
68
|
+
async sendVerificationOTP({ email, otp, type }) {
|
|
69
|
+
if (type === 'email-verification') {
|
|
70
|
+
await sendEmail({
|
|
71
|
+
to: email,
|
|
72
|
+
subject: "Your verification code",
|
|
73
|
+
html: `
|
|
74
|
+
<h1>Verify your email</h1>
|
|
75
|
+
<p>Your verification code is:</p>
|
|
76
|
+
<h2 style="font-size: 32px; letter-spacing: 8px; font-family: monospace;">${otp}</h2>
|
|
77
|
+
<p>This code expires in 5 minutes.</p>
|
|
78
|
+
`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
77
83
|
<% } %>
|
|
78
84
|
],
|
|
79
85
|
session: {
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
"description": "Backend API for <%= projectName %>",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "controllers/rest-api/index.ts",
|
|
7
|
+
"overrides": {
|
|
8
|
+
"expo-network": "npm:empty-npm-package@1.0.0",
|
|
9
|
+
"expo-secure-store": "npm:empty-npm-package@1.0.0",
|
|
10
|
+
"@react-native-async-storage/async-storage": "npm:empty-npm-package@1.0.0",
|
|
11
|
+
"react-native": "npm:empty-npm-package@1.0.0"
|
|
12
|
+
},
|
|
7
13
|
"scripts": {
|
|
8
14
|
"dev:rest-api": "bun --watch ./controllers/rest-api/index.ts | pino-pretty --colorize",<% if (backend.eventQueue) { %>
|
|
9
15
|
"dev:event-queue": "bun --watch ./controllers/event-queue/index.ts | pino-pretty --colorize",<% } %>
|
|
@@ -21,30 +27,30 @@
|
|
|
21
27
|
"db:studio": "drizzle-kit studio"<% } %>
|
|
22
28
|
},
|
|
23
29
|
"dependencies": {
|
|
24
|
-
"@fastify/cors": "
|
|
25
|
-
"@prisma/adapter-pg": "
|
|
26
|
-
"@prisma/client": "
|
|
27
|
-
"drizzle-orm": "
|
|
28
|
-
"pg": "
|
|
29
|
-
"@sinclair/typebox": "
|
|
30
|
-
"ajv": "
|
|
31
|
-
"better-auth": "
|
|
32
|
-
"@better-auth/expo": "
|
|
33
|
-
"dotenv": "
|
|
34
|
-
"fastify": "
|
|
35
|
-
"fastify-plugin": "
|
|
36
|
-
"ioredis": "
|
|
37
|
-
"pino-pretty": "
|
|
38
|
-
"bullmq": "
|
|
39
|
-
"nodemailer": "
|
|
30
|
+
"@fastify/cors": "~11.0.1",<% if (backend.orm === 'prisma') { %>
|
|
31
|
+
"@prisma/adapter-pg": "~7.0.0",
|
|
32
|
+
"@prisma/client": "~7.0.0",<% } %><% if (backend.orm === 'drizzle') { %>
|
|
33
|
+
"drizzle-orm": "~0.44.0",
|
|
34
|
+
"pg": "~8.13.0",<% } %>
|
|
35
|
+
"@sinclair/typebox": "~0.34.33",
|
|
36
|
+
"ajv": "~8.17.1",
|
|
37
|
+
"better-auth": "~1.4.5",
|
|
38
|
+
"@better-auth/expo": "~1.4.5",
|
|
39
|
+
"dotenv": "~16.5.0",
|
|
40
|
+
"fastify": "~5.3.3",
|
|
41
|
+
"fastify-plugin": "~5.0.1",
|
|
42
|
+
"ioredis": "~5.4.1",
|
|
43
|
+
"pino-pretty": "~13.0.0"<% if (backend.eventQueue) { %>,
|
|
44
|
+
"bullmq": "~5.40.3"<% } %><% if (features.authentication.emailVerification || features.authentication.passwordReset) { %>,
|
|
45
|
+
"nodemailer": "~6.9.0"<% } %>
|
|
40
46
|
},
|
|
41
47
|
"devDependencies": {
|
|
42
|
-
"@types/node": "
|
|
43
|
-
"prisma": "
|
|
44
|
-
"drizzle-kit": "
|
|
45
|
-
"@types/pg": "
|
|
46
|
-
"tsx": "
|
|
47
|
-
"typescript": "
|
|
48
|
-
"@types/nodemailer": "
|
|
48
|
+
"@types/node": "~24.0.0",<% if (backend.orm === 'prisma') { %>
|
|
49
|
+
"prisma": "~7.0.0",<% } %><% if (backend.orm === 'drizzle') { %>
|
|
50
|
+
"drizzle-kit": "~0.30.0",
|
|
51
|
+
"@types/pg": "~8.11.0",<% } %>
|
|
52
|
+
"tsx": "~4.20.1",
|
|
53
|
+
"typescript": "~5.8.3"<% if (features.authentication.emailVerification || features.authentication.passwordReset) { %>,
|
|
54
|
+
"@types/nodemailer": "~6.4.0"<% } %>
|
|
49
55
|
}
|
|
50
56
|
}
|
|
@@ -20,10 +20,16 @@ model User {
|
|
|
20
20
|
image String?
|
|
21
21
|
createdAt DateTime @default(now())
|
|
22
22
|
updatedAt DateTime @updatedAt
|
|
23
|
+
<% if (features.authentication.twoFactor) { %>
|
|
24
|
+
twoFactorEnabled Boolean @default(false)
|
|
25
|
+
<% } %>
|
|
23
26
|
|
|
24
27
|
// BetterAuth relations
|
|
25
28
|
sessions Session[]
|
|
26
29
|
accounts Account[]
|
|
30
|
+
<% if (features.authentication.twoFactor) { %>
|
|
31
|
+
twoFactor TwoFactor[]
|
|
32
|
+
<% } %>
|
|
27
33
|
|
|
28
34
|
// Device session migration relation
|
|
29
35
|
deviceSessions DeviceSession[]
|
|
@@ -100,3 +106,17 @@ model DeviceSession {
|
|
|
100
106
|
|
|
101
107
|
@@map("device_session")
|
|
102
108
|
}
|
|
109
|
+
|
|
110
|
+
<% if (features.authentication.twoFactor) { %>
|
|
111
|
+
// BetterAuth TwoFactor model
|
|
112
|
+
model TwoFactor {
|
|
113
|
+
id String @id @default(cuid())
|
|
114
|
+
userId String
|
|
115
|
+
secret String?
|
|
116
|
+
backupCodes String?
|
|
117
|
+
|
|
118
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
119
|
+
|
|
120
|
+
@@map("two_factor")
|
|
121
|
+
}
|
|
122
|
+
<% } %>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { View, Text, StyleSheet } from 'react-native';
|
|
3
3
|
import { router } from 'expo-router';
|
|
4
|
-
import { Button } from '../src/components/ui';
|
|
4
|
+
import { Button } from '../src/components/ui/button';
|
|
5
5
|
|
|
6
6
|
export default function NotFoundScreen() {
|
|
7
7
|
const handleGoHome = () => {
|
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
import { Stack, router } from 'expo-router';
|
|
2
|
-
import { useEffect<% if (features.sessionManagement) { %>, useState<% } %> } from 'react';
|
|
1
|
+
import { Stack, router<% if (features.authentication.enabled) { %>, useSegments<% } %> } from 'expo-router';
|
|
2
|
+
import { useEffect, useRef<% if (!features.sessionManagement) { %>, useState<% } %> } from 'react';
|
|
3
|
+
import { AppState, AppStateStatus } from 'react-native';
|
|
3
4
|
import Constants from 'expo-constants';
|
|
4
|
-
import { ThemeProvider } from '@/context/
|
|
5
|
+
import { ThemeProvider } from '@/context/theme-context';
|
|
6
|
+
import { Toast } from '@/components/ui/toast';
|
|
5
7
|
<% 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/
|
|
7
|
-
<% } %><% if (features.sessionManagement) { %>import { useSessionActions, useSession } from '../src/store/
|
|
8
|
+
<% } %><% if (integrations.revenueCat.enabled || integrations.adjust.enabled || integrations.scate.enabled) { %>import { initializeSDKs } from '@/services/sdk-initializer';
|
|
9
|
+
<% } %><% if (features.sessionManagement) { %>import { useSessionActions, useSession } from '../src/store/device-session-store';
|
|
10
|
+
<% } %><% if (features.authentication.enabled) { %>import { useAuth } from '../src/hooks/auth';
|
|
8
11
|
<% } %>import { logger } from '../src/utils/logger';
|
|
9
12
|
|
|
10
13
|
export default function RootLayout() {
|
|
11
|
-
<% if (features.sessionManagement) { %> const { initializeSession } = useSessionActions();
|
|
14
|
+
<% if (features.sessionManagement) { %> const { initializeSession, isSessionValid } = useSessionActions();
|
|
12
15
|
const { isSessionChecked } = useSession();
|
|
13
16
|
<% } else { %> const [isReady, setIsReady] = useState(false);
|
|
17
|
+
<% } %><% if (features.authentication.enabled) { %> const { isAuthenticated, isPending: isAuthPending<% if (features.authentication.emailVerification) { %>, user<% } %> } = useAuth();
|
|
18
|
+
const segments = useSegments();
|
|
14
19
|
<% } %>
|
|
20
|
+
const appState = useRef(AppState.currentState);
|
|
21
|
+
|
|
15
22
|
// Get feature flags from app.json
|
|
16
23
|
const onboardingEnabled = Constants.expoConfig?.extra?.features?.onboarding?.enabled ?? false;
|
|
17
24
|
|
|
@@ -44,7 +51,12 @@ export default function RootLayout() {
|
|
|
44
51
|
logger.info('RootLayout: Initializing session');
|
|
45
52
|
await initializeSession();
|
|
46
53
|
<% } %>
|
|
47
|
-
|
|
54
|
+
<% if (features.authentication.enabled) { %> // Route based on authentication state
|
|
55
|
+
// Note: The actual routing will happen in the useEffect below once auth state is known
|
|
56
|
+
// For now, just mark initialization complete
|
|
57
|
+
logger.info('RootLayout: Waiting for auth state...');
|
|
58
|
+
<% } else { %> router.replace('/(tabs)');
|
|
59
|
+
<% } %>
|
|
48
60
|
} catch (error) {
|
|
49
61
|
logger.error('RootLayout: Failed to initialize app', { error });
|
|
50
62
|
// Fallback to tabs on error
|
|
@@ -57,15 +69,88 @@ export default function RootLayout() {
|
|
|
57
69
|
initializeApp();
|
|
58
70
|
}, [<% if (features.sessionManagement) { %>isSessionChecked, initializeSession, <% } %>onboardingEnabled]);
|
|
59
71
|
|
|
72
|
+
<% if (features.sessionManagement) { %>
|
|
73
|
+
// Revalidate session when app comes to foreground
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const subscription = AppState.addEventListener('change', (nextAppState: AppStateStatus) => {
|
|
76
|
+
// App came to foreground from background/inactive
|
|
77
|
+
if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
|
|
78
|
+
logger.debug('RootLayout: App came to foreground, revalidating session');
|
|
79
|
+
isSessionValid();
|
|
80
|
+
}
|
|
81
|
+
appState.current = nextAppState;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return () => subscription.remove();
|
|
85
|
+
}, [isSessionValid]);
|
|
86
|
+
<% } %>
|
|
87
|
+
<% if (features.authentication.enabled) { %>
|
|
88
|
+
// Route based on authentication state<% if (features.authentication.emailVerification) { %> and email verification<% } %>
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
// Wait for auth state to be determined
|
|
91
|
+
if (isAuthPending) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Get current route group/screen
|
|
96
|
+
const currentRoute = segments[0];
|
|
97
|
+
const inAuthGroup = currentRoute === '(auth)';
|
|
98
|
+
const inPublicGroup = currentRoute === '(public)';
|
|
99
|
+
const inTabsGroup = currentRoute === '(tabs)';
|
|
100
|
+
<% if (features.onboarding.enabled) { %> const inOnboardingGroup = currentRoute === '(onboarding)';
|
|
101
|
+
<% } %><% if (features.authentication.emailVerification) { %> const onVerifyEmail = currentRoute === 'verify-email';
|
|
102
|
+
<% } %> const onAccountScreen = currentRoute === 'account';
|
|
103
|
+
|
|
104
|
+
// Don't interfere with<% if (features.onboarding.enabled) { %> onboarding,<% } %> auth modal, or account screen flows
|
|
105
|
+
if (<% if (features.onboarding.enabled) { %>inOnboardingGroup || <% } %>inAuthGroup || onAccountScreen) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
logger.info('RootLayout: Auth state determined', {
|
|
110
|
+
isAuthenticated,
|
|
111
|
+
<% if (features.authentication.emailVerification) { %> emailVerified: user?.emailVerified,
|
|
112
|
+
<% } %> currentRoute
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (isAuthenticated) {
|
|
116
|
+
<% if (features.authentication.emailVerification) { %> // Check if email verification is required
|
|
117
|
+
if (user && user.emailVerified === false) {
|
|
118
|
+
// Unverified - navigate to full-screen verify-email
|
|
119
|
+
if (!onVerifyEmail) {
|
|
120
|
+
router.replace(`/verify-email?email=${encodeURIComponent(user.email)}`);
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
// Verified - navigate to tabs
|
|
124
|
+
if (!inTabsGroup) {
|
|
125
|
+
router.replace('/(tabs)');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
<% } else { %> // Only navigate if not already in tabs
|
|
129
|
+
if (!inTabsGroup) {
|
|
130
|
+
router.replace('/(tabs)');
|
|
131
|
+
}
|
|
132
|
+
<% } %> } else {
|
|
133
|
+
// Not authenticated - navigate to public
|
|
134
|
+
if (!inPublicGroup) {
|
|
135
|
+
router.replace('/(public)');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}, [isAuthenticated, isAuthPending, <% if (features.authentication.emailVerification) { %>user, <% } %>segments]);
|
|
139
|
+
<% } %>
|
|
140
|
+
|
|
60
141
|
return (
|
|
61
142
|
<ThemeProvider>
|
|
62
143
|
<Stack screenOptions={{ headerShown: false }}>
|
|
63
144
|
<% if (features.onboarding.enabled) { %> <Stack.Screen name="(onboarding)" />
|
|
64
|
-
<% } %><% if (features.authentication.enabled) { %> <Stack.Screen name="(
|
|
65
|
-
|
|
145
|
+
<% } %><% if (features.authentication.enabled) { %> <Stack.Screen name="(public)" />
|
|
146
|
+
<Stack.Screen name="(auth)" options={{ presentation: 'modal' }} />
|
|
147
|
+
<% if (features.authentication.emailVerification) { %> <Stack.Screen name="verify-email" />
|
|
148
|
+
<% } %><% } %><% if (features.paywall) { %> <Stack.Screen name="paywall" options={{ presentation: 'modal' }} />
|
|
66
149
|
<% } %> <Stack.Screen name="(tabs)" />
|
|
67
|
-
<Stack.Screen name="
|
|
150
|
+
<% if (features.authentication.enabled) { %> <Stack.Screen name="account" options={{ presentation: 'card' }} />
|
|
151
|
+
<% } %> <Stack.Screen name="+not-found" />
|
|
68
152
|
</Stack>
|
|
153
|
+
<Toast />
|
|
69
154
|
</ThemeProvider>
|
|
70
155
|
);
|
|
71
156
|
}
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"description": "React Native mobile app for <%= projectName %>",
|
|
5
5
|
"main": "expo-router/entry",
|
|
6
|
+
"overrides": {
|
|
7
|
+
"react-dom": "npm:empty-npm-package@1.0.0"
|
|
8
|
+
},
|
|
6
9
|
"scripts": {
|
|
7
10
|
"start": "expo start",
|
|
8
11
|
"android": "expo run:android",
|
|
@@ -12,8 +15,8 @@
|
|
|
12
15
|
"type-check": "tsc --noEmit"
|
|
13
16
|
},
|
|
14
17
|
"dependencies": {
|
|
15
|
-
"@expo/vector-icons": "
|
|
16
|
-
"@react-native-async-storage/async-storage": "2.2.0",
|
|
18
|
+
"@expo/vector-icons": "~15.0.3",
|
|
19
|
+
"@react-native-async-storage/async-storage": "~2.2.0",
|
|
17
20
|
"expo": "~54.0.0",
|
|
18
21
|
"expo-application": "~7.0.7",
|
|
19
22
|
"expo-constants": "~18.0.10",
|
|
@@ -28,24 +31,29 @@
|
|
|
28
31
|
"react-native": "0.81.5",
|
|
29
32
|
"react-native-safe-area-context": "~5.6.0",
|
|
30
33
|
"react-native-screens": "~4.16.0",
|
|
31
|
-
"zustand": "
|
|
32
|
-
"axios": "
|
|
34
|
+
"zustand": "~5.0.5"<% if (features.authentication.enabled) { %>,
|
|
35
|
+
"axios": "~1.9.0",
|
|
33
36
|
"expo-secure-store": "~15.0.7",
|
|
34
37
|
"expo-network": "~8.0.7",
|
|
35
|
-
"better-auth": "
|
|
36
|
-
"@better-auth/expo": "
|
|
38
|
+
"better-auth": "~1.4.5",
|
|
39
|
+
"@better-auth/expo": "~1.4.5"<% } %><% if (features.authentication.providers.google || features.authentication.providers.apple || features.authentication.providers.github) { %>,
|
|
37
40
|
"expo-web-browser": "~15.0.7"<% } %><% if (features.authentication.providers.google) { %>,
|
|
38
|
-
"@react-native-google-signin/google-signin": "
|
|
41
|
+
"@react-native-google-signin/google-signin": "~13.1.0"<% } %><% if (features.authentication.providers.apple) { %>,
|
|
39
42
|
"expo-apple-authentication": "~7.1.3"<% } %><% if (integrations.revenueCat.enabled) { %>,
|
|
40
|
-
"react-native-purchases": "
|
|
41
|
-
"react-native-adjust": "
|
|
42
|
-
"scatesdk-react": "
|
|
43
|
-
"expo-tracking-transparency": "~5.2.4"<% }
|
|
43
|
+
"react-native-purchases": "~9.1.0"<% } %><% if (integrations.adjust.enabled) { %>,
|
|
44
|
+
"react-native-adjust": "~5.4.1"<% } %><% if (integrations.scate.enabled) { %>,
|
|
45
|
+
"scatesdk-react": "~0.4.12"<% } %><% if (integrations.att.enabled) { %>,
|
|
46
|
+
"expo-tracking-transparency": "~5.2.4"<% } %><% if (features.authentication.twoFactor) { %>,
|
|
47
|
+
"react-native-svg": "~15.12.1",
|
|
48
|
+
"react-native-qrcode-svg": "~6.3.2",
|
|
49
|
+
"expo-clipboard": "~8.0.8",
|
|
50
|
+
"expo-file-system": "~19.0.21",
|
|
51
|
+
"expo-sharing": "~14.0.8"<% } %>
|
|
44
52
|
},
|
|
45
53
|
"devDependencies": {
|
|
46
|
-
"@babel/core": "^7.
|
|
54
|
+
"@babel/core": "^7.26.0",
|
|
47
55
|
"@types/react": "~19.1.10",
|
|
48
|
-
"eslint": "
|
|
56
|
+
"eslint": "~9.25.0",
|
|
49
57
|
"eslint-config-expo": "~10.0.0",
|
|
50
58
|
"typescript": "~5.9.2"
|
|
51
59
|
},
|
|
@@ -9,12 +9,12 @@ import {
|
|
|
9
9
|
PressableProps,
|
|
10
10
|
Animated,
|
|
11
11
|
} from 'react-native';
|
|
12
|
-
import { useAppTheme, AppTheme } from '@/context/
|
|
12
|
+
import { useAppTheme, AppTheme } from '@/context/theme-context';
|
|
13
13
|
|
|
14
14
|
interface ButtonProps extends Omit<PressableProps, 'style'> {
|
|
15
15
|
title: string;
|
|
16
16
|
loading?: boolean;
|
|
17
|
-
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
|
|
17
|
+
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive';
|
|
18
18
|
size?: 'small' | 'medium' | 'large';
|
|
19
19
|
fullWidth?: boolean;
|
|
20
20
|
style?: ViewStyle;
|
|
@@ -79,7 +79,7 @@ export const Button: React.FC<ButtonProps> = ({
|
|
|
79
79
|
{loading ? (
|
|
80
80
|
<ActivityIndicator
|
|
81
81
|
size="small"
|
|
82
|
-
color={variant === 'primary' ? theme.colors.textInverse : theme.colors.primary}
|
|
82
|
+
color={variant === 'primary' || variant === 'destructive' ? theme.colors.textInverse : theme.colors.primary}
|
|
83
83
|
/>
|
|
84
84
|
) : (
|
|
85
85
|
<Text style={[
|
|
@@ -109,6 +109,7 @@ const createStyles = (theme: AppTheme) => StyleSheet.create({
|
|
|
109
109
|
secondary: { backgroundColor: theme.colors.backgroundSecondary },
|
|
110
110
|
outline: { backgroundColor: 'transparent', borderColor: theme.colors.borderStrong },
|
|
111
111
|
ghost: { backgroundColor: 'transparent' },
|
|
112
|
+
destructive: { backgroundColor: theme.colors.error, borderColor: theme.colors.error },
|
|
112
113
|
|
|
113
114
|
small: { paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], minHeight: 36 },
|
|
114
115
|
medium: { paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[3], minHeight: 48 },
|
|
@@ -122,6 +123,7 @@ const createStyles = (theme: AppTheme) => StyleSheet.create({
|
|
|
122
123
|
secondaryText: { color: theme.colors.text },
|
|
123
124
|
outlineText: { color: theme.colors.text },
|
|
124
125
|
ghostText: { color: theme.colors.primary },
|
|
126
|
+
destructiveText: { color: theme.colors.textInverse },
|
|
125
127
|
|
|
126
128
|
smallText: { fontSize: theme.typography.fontSize.sm },
|
|
127
129
|
mediumText: { fontSize: theme.typography.fontSize.base },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useRef, useMemo } from 'react';
|
|
2
2
|
import { View, Text, StyleSheet, ViewStyle, Pressable, Animated } from 'react-native';
|
|
3
|
-
import { useAppTheme, AppTheme } from '@/context/
|
|
3
|
+
import { useAppTheme, AppTheme } from '@/context/theme-context';
|
|
4
4
|
|
|
5
5
|
interface CardProps {
|
|
6
6
|
children: React.ReactNode;
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
Pressable,
|
|
11
11
|
Animated,
|
|
12
12
|
} from 'react-native';
|
|
13
|
-
import { useAppTheme, AppTheme } from '@/context/
|
|
13
|
+
import { useAppTheme, AppTheme } from '@/context/theme-context';
|
|
14
14
|
|
|
15
15
|
interface InputProps extends Omit<TextInputProps, 'style'> {
|
|
16
16
|
label?: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useMemo } from 'react';
|
|
2
2
|
import { View, StyleSheet, Animated, ViewStyle } from 'react-native';
|
|
3
|
-
import { useAppTheme, AppTheme } from '@/context/
|
|
3
|
+
import { useAppTheme, AppTheme } from '@/context/theme-context';
|
|
4
4
|
|
|
5
5
|
interface SkeletonProps {
|
|
6
6
|
width?: number | string;
|