@better-t-stack/template-generator 3.20.1 → 3.21.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/dist/index.mjs CHANGED
@@ -705,7 +705,10 @@ const dependencyVersionMap = {
705
705
  "@tanstack/vue-query": "^5.90.2",
706
706
  "@tanstack/react-query-devtools": "^5.91.1",
707
707
  "@tanstack/react-query": "^5.90.12",
708
+ "@tanstack/react-form": "^1.28.0",
708
709
  "@tanstack/react-router-ssr-query": "^1.142.7",
710
+ "@tanstack/solid-form": "^1.28.0",
711
+ "@tanstack/svelte-form": "^1.28.0",
709
712
  "@tanstack/solid-query": "^5.87.4",
710
713
  "@tanstack/solid-query-devtools": "^5.87.4",
711
714
  "@tanstack/solid-router-devtools": "^1.131.44",
@@ -1261,6 +1264,9 @@ function processConvexAuthDeps(vfs, config) {
1261
1264
  const hasNextJs = frontend.includes("next");
1262
1265
  const hasTanStackStart = frontend.includes("tanstack-start");
1263
1266
  const hasViteReact = frontend.some((f) => ["tanstack-router", "react-router"].includes(f));
1267
+ const hasSolid = frontend.includes("solid");
1268
+ const hasSvelte = frontend.includes("svelte");
1269
+ const hasReactWebAuthForms = hasNextJs || hasTanStackStart || hasViteReact;
1264
1270
  if (auth === "clerk") {
1265
1271
  if (webExists) {
1266
1272
  if (hasNextJs) addPackageDependency({
@@ -1299,19 +1305,37 @@ function processConvexAuthDeps(vfs, config) {
1299
1305
  customDependencies: { "@better-auth/expo": "1.4.9" }
1300
1306
  });
1301
1307
  }
1302
- if (webExists) addPackageDependency({
1303
- vfs,
1304
- packagePath: webPath,
1305
- dependencies: ["better-auth", "@convex-dev/better-auth"],
1306
- customDependencies: { "better-auth": "1.4.9" }
1307
- });
1308
+ if (webExists) {
1309
+ addPackageDependency({
1310
+ vfs,
1311
+ packagePath: webPath,
1312
+ dependencies: ["better-auth", "@convex-dev/better-auth"],
1313
+ customDependencies: { "better-auth": "1.4.9" }
1314
+ });
1315
+ if (hasReactWebAuthForms) addPackageDependency({
1316
+ vfs,
1317
+ packagePath: webPath,
1318
+ dependencies: ["@tanstack/react-form"]
1319
+ });
1320
+ if (hasSolid) addPackageDependency({
1321
+ vfs,
1322
+ packagePath: webPath,
1323
+ dependencies: ["@tanstack/solid-form"]
1324
+ });
1325
+ if (hasSvelte) addPackageDependency({
1326
+ vfs,
1327
+ packagePath: webPath,
1328
+ dependencies: ["@tanstack/svelte-form"]
1329
+ });
1330
+ }
1308
1331
  if (nativeExists && hasNative) addPackageDependency({
1309
1332
  vfs,
1310
1333
  packagePath: nativePath,
1311
1334
  dependencies: [
1312
1335
  "better-auth",
1313
1336
  "@better-auth/expo",
1314
- "@convex-dev/better-auth"
1337
+ "@convex-dev/better-auth",
1338
+ "@tanstack/react-form"
1315
1339
  ],
1316
1340
  customDependencies: {
1317
1341
  "better-auth": "1.4.9",
@@ -1343,6 +1367,14 @@ function processStandardAuthDeps(vfs, config) {
1343
1367
  "solid",
1344
1368
  "astro"
1345
1369
  ].includes(f));
1370
+ const hasReactWebAuthForms = frontend.some((f) => [
1371
+ "react-router",
1372
+ "tanstack-router",
1373
+ "tanstack-start",
1374
+ "next"
1375
+ ].includes(f));
1376
+ const hasSolid = frontend.includes("solid");
1377
+ const hasSvelte = frontend.includes("svelte");
1346
1378
  if (auth === "better-auth") {
1347
1379
  if (authExists) {
1348
1380
  addPackageDependency({
@@ -1356,15 +1388,36 @@ function processStandardAuthDeps(vfs, config) {
1356
1388
  dependencies: ["@better-auth/expo"]
1357
1389
  });
1358
1390
  }
1359
- if (hasWebFrontend && webExists) addPackageDependency({
1360
- vfs,
1361
- packagePath: webPath,
1362
- dependencies: ["better-auth"]
1363
- });
1391
+ if (hasWebFrontend && webExists) {
1392
+ addPackageDependency({
1393
+ vfs,
1394
+ packagePath: webPath,
1395
+ dependencies: ["better-auth"]
1396
+ });
1397
+ if (hasReactWebAuthForms) addPackageDependency({
1398
+ vfs,
1399
+ packagePath: webPath,
1400
+ dependencies: ["@tanstack/react-form"]
1401
+ });
1402
+ if (hasSolid) addPackageDependency({
1403
+ vfs,
1404
+ packagePath: webPath,
1405
+ dependencies: ["@tanstack/solid-form"]
1406
+ });
1407
+ if (hasSvelte) addPackageDependency({
1408
+ vfs,
1409
+ packagePath: webPath,
1410
+ dependencies: ["@tanstack/svelte-form"]
1411
+ });
1412
+ }
1364
1413
  if (hasNative && nativeExists) addPackageDependency({
1365
1414
  vfs,
1366
1415
  packagePath: nativePath,
1367
- dependencies: ["better-auth", "@better-auth/expo"]
1416
+ dependencies: [
1417
+ "better-auth",
1418
+ "@better-auth/expo",
1419
+ "@tanstack/react-form"
1420
+ ]
1368
1421
  });
1369
1422
  }
1370
1423
  }
@@ -1513,10 +1566,8 @@ function processPrismaDeps(vfs, config, dbPkgPath, webPkgPath, webExists) {
1513
1566
  if (database === "mysql" && dbSetup === "planetscale") deps.push("@prisma/adapter-planetscale", "@planetscale/database");
1514
1567
  else if (database === "mysql") deps.push("@prisma/adapter-mariadb");
1515
1568
  else if (database === "sqlite") deps.push(dbSetup === "d1" ? "@prisma/adapter-d1" : "@prisma/adapter-libsql");
1516
- else if (database === "postgres") if (dbSetup === "neon") {
1517
- deps.push("@prisma/adapter-neon", "@neondatabase/serverless", "ws");
1518
- devDeps.push("@types/ws");
1519
- } else if (dbSetup === "prisma-postgres") {
1569
+ else if (database === "postgres") if (dbSetup === "neon") deps.push("@prisma/adapter-neon", "@neondatabase/serverless");
1570
+ else if (dbSetup === "prisma-postgres") {
1520
1571
  deps.push("@prisma/adapter-pg", "pg");
1521
1572
  devDeps.push("@types/pg");
1522
1573
  } else {
@@ -1556,10 +1607,8 @@ function processDrizzleDeps(vfs, config, dbPkgPath, webPkgPath, webExists) {
1556
1607
  } else if (database === "postgres") {
1557
1608
  const deps = ["drizzle-orm"];
1558
1609
  const devDeps = ["drizzle-kit"];
1559
- if (dbSetup === "neon") {
1560
- deps.push("@neondatabase/serverless", "ws");
1561
- devDeps.push("@types/ws");
1562
- } else {
1610
+ if (dbSetup === "neon") deps.push("@neondatabase/serverless");
1611
+ else {
1563
1612
  deps.push("pg");
1564
1613
  devDeps.push("@types/pg");
1565
1614
  }
@@ -5139,91 +5188,187 @@ export const get = query({
5139
5188
  });
5140
5189
  `],
5141
5190
  ["auth/better-auth/convex/native/bare/components/sign-in.tsx.hbs", `import { authClient } from "@/lib/auth-client";
5191
+ import { useForm } from "@tanstack/react-form";
5142
5192
  import { useState } from "react";
5143
5193
  import {
5144
5194
  ActivityIndicator,
5195
+ StyleSheet,
5145
5196
  Text,
5146
5197
  TextInput,
5147
5198
  TouchableOpacity,
5148
5199
  View,
5149
- StyleSheet,
5150
5200
  } from "react-native";
5151
- import { useColorScheme } from "@/lib/use-color-scheme";
5152
5201
  import { NAV_THEME } from "@/lib/constants";
5202
+ import { useColorScheme } from "@/lib/use-color-scheme";
5203
+ import z from "zod";
5204
+
5205
+ const signInSchema = z.object({
5206
+ email: z
5207
+ .string()
5208
+ .trim()
5209
+ .min(1, "Email is required")
5210
+ .email("Enter a valid email address"),
5211
+ password: z
5212
+ .string()
5213
+ .min(1, "Password is required")
5214
+ .min(8, "Use at least 8 characters"),
5215
+ });
5216
+
5217
+ function getErrorMessage(error: unknown): string | null {
5218
+ if (!error) return null;
5219
+
5220
+ if (typeof error === "string") {
5221
+ return error;
5222
+ }
5223
+
5224
+ if (Array.isArray(error)) {
5225
+ for (const issue of error) {
5226
+ const message = getErrorMessage(issue);
5227
+ if (message) {
5228
+ return message;
5229
+ }
5230
+ }
5231
+ return null;
5232
+ }
5233
+
5234
+ if (typeof error === "object" && error !== null) {
5235
+ const maybeError = error as { message?: unknown };
5236
+ if (typeof maybeError.message === "string") {
5237
+ return maybeError.message;
5238
+ }
5239
+ }
5240
+
5241
+ return null;
5242
+ }
5153
5243
 
5154
5244
  function SignIn() {
5155
5245
  const { colorScheme } = useColorScheme();
5156
5246
  const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light;
5157
- const [email, setEmail] = useState("");
5158
- const [password, setPassword] = useState("");
5159
- const [isLoading, setIsLoading] = useState(false);
5160
5247
  const [error, setError] = useState<string | null>(null);
5161
5248
 
5162
- async function handleLogin() {
5163
- setIsLoading(true);
5164
- setError(null);
5165
-
5166
- await authClient.signIn.email(
5167
- {
5168
- email,
5169
- password,
5170
- },
5171
- {
5172
- onError(error) {
5173
- setError(error.error?.message || "Failed to sign in");
5174
- setIsLoading(false);
5175
- },
5176
- onSuccess() {
5177
- setEmail("");
5178
- setPassword("");
5249
+ const form = useForm({
5250
+ defaultValues: {
5251
+ email: "",
5252
+ password: "",
5253
+ },
5254
+ validators: {
5255
+ onSubmit: signInSchema,
5256
+ },
5257
+ onSubmit: async ({ value, formApi }) => {
5258
+ await authClient.signIn.email(
5259
+ {
5260
+ email: value.email.trim(),
5261
+ password: value.password,
5179
5262
  },
5180
- onFinished() {
5181
- setIsLoading(false);
5263
+ {
5264
+ onError(error) {
5265
+ setError(error.error?.message || "Failed to sign in");
5266
+ },
5267
+ onSuccess() {
5268
+ setError(null);
5269
+ formApi.reset();
5270
+ },
5182
5271
  },
5183
- }
5184
- );
5185
- }
5272
+ );
5273
+ },
5274
+ });
5186
5275
 
5187
5276
  return (
5188
5277
  <View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
5189
5278
  <Text style={[styles.title, { color: theme.text }]}>Sign In</Text>
5190
5279
 
5191
- {error ? (
5192
- <View style={[styles.errorContainer, { backgroundColor: theme.notification + "20" }]}>
5193
- <Text style={[styles.errorText, { color: theme.notification }]}>{error}</Text>
5194
- </View>
5195
- ) : null}
5280
+ <form.Subscribe
5281
+ selector={(state) => ({
5282
+ isSubmitting: state.isSubmitting,
5283
+ validationError: getErrorMessage(state.errorMap.onSubmit),
5284
+ })}
5285
+ >
5286
+ {({ isSubmitting, validationError }) => {
5287
+ const formError = error ?? validationError;
5196
5288
 
5197
- <TextInput
5198
- style={[styles.input, { color: theme.text, borderColor: theme.border, backgroundColor: theme.background }]}
5199
- placeholder="Email"
5200
- placeholderTextColor={theme.text}
5201
- value={email}
5202
- onChangeText={setEmail}
5203
- keyboardType="email-address"
5204
- autoCapitalize="none"
5205
- />
5289
+ return (
5290
+ <>
5291
+ {formError ? (
5292
+ <View style={[styles.errorContainer, { backgroundColor: theme.notification + "20" }]}>
5293
+ <Text style={[styles.errorText, { color: theme.notification }]}>{formError}</Text>
5294
+ </View>
5295
+ ) : null}
5206
5296
 
5207
- <TextInput
5208
- style={[styles.input, { color: theme.text, borderColor: theme.border, backgroundColor: theme.background }]}
5209
- placeholder="Password"
5210
- placeholderTextColor={theme.text}
5211
- value={password}
5212
- onChangeText={setPassword}
5213
- secureTextEntry
5214
- />
5297
+ <form.Field name="email">
5298
+ {(field) => (
5299
+ <TextInput
5300
+ style={[
5301
+ styles.input,
5302
+ {
5303
+ color: theme.text,
5304
+ borderColor: theme.border,
5305
+ backgroundColor: theme.background,
5306
+ },
5307
+ ]}
5308
+ placeholder="Email"
5309
+ placeholderTextColor={theme.text}
5310
+ value={field.state.value}
5311
+ onBlur={field.handleBlur}
5312
+ onChangeText={(value) => {
5313
+ field.handleChange(value);
5314
+ if (error) {
5315
+ setError(null);
5316
+ }
5317
+ }}
5318
+ keyboardType="email-address"
5319
+ autoCapitalize="none"
5320
+ />
5321
+ )}
5322
+ </form.Field>
5215
5323
 
5216
- <TouchableOpacity
5217
- onPress={handleLogin}
5218
- disabled={isLoading}
5219
- style={[styles.button, { backgroundColor: theme.primary, opacity: isLoading ? 0.5 : 1 }]}
5220
- >
5221
- {isLoading ? (
5222
- <ActivityIndicator size="small" color="#ffffff" />
5223
- ) : (
5224
- <Text style={styles.buttonText}>Sign In</Text>
5225
- )}
5226
- </TouchableOpacity>
5324
+ <form.Field name="password">
5325
+ {(field) => (
5326
+ <TextInput
5327
+ style={[
5328
+ styles.input,
5329
+ {
5330
+ color: theme.text,
5331
+ borderColor: theme.border,
5332
+ backgroundColor: theme.background,
5333
+ },
5334
+ ]}
5335
+ placeholder="Password"
5336
+ placeholderTextColor={theme.text}
5337
+ value={field.state.value}
5338
+ onBlur={field.handleBlur}
5339
+ onChangeText={(value) => {
5340
+ field.handleChange(value);
5341
+ if (error) {
5342
+ setError(null);
5343
+ }
5344
+ }}
5345
+ secureTextEntry
5346
+ onSubmitEditing={form.handleSubmit}
5347
+ />
5348
+ )}
5349
+ </form.Field>
5350
+
5351
+ <TouchableOpacity
5352
+ onPress={form.handleSubmit}
5353
+ disabled={isSubmitting}
5354
+ style={[
5355
+ styles.button,
5356
+ {
5357
+ backgroundColor: theme.primary,
5358
+ opacity: isSubmitting ? 0.5 : 1,
5359
+ },
5360
+ ]}
5361
+ >
5362
+ {isSubmitting ? (
5363
+ <ActivityIndicator size="small" color="#ffffff" />
5364
+ ) : (
5365
+ <Text style={styles.buttonText}>Sign In</Text>
5366
+ )}
5367
+ </TouchableOpacity>
5368
+ </>
5369
+ );
5370
+ }}
5371
+ </form.Subscribe>
5227
5372
  </View>
5228
5373
  );
5229
5374
  }
@@ -5264,105 +5409,221 @@ const styles = StyleSheet.create({
5264
5409
  });
5265
5410
 
5266
5411
  export { SignIn };
5267
-
5268
5412
  `],
5269
5413
  ["auth/better-auth/convex/native/bare/components/sign-up.tsx.hbs", `import { authClient } from "@/lib/auth-client";
5414
+ import { useForm } from "@tanstack/react-form";
5270
5415
  import { useState } from "react";
5271
5416
  import {
5272
5417
  ActivityIndicator,
5418
+ StyleSheet,
5273
5419
  Text,
5274
5420
  TextInput,
5275
5421
  TouchableOpacity,
5276
5422
  View,
5277
- StyleSheet,
5278
5423
  } from "react-native";
5279
- import { useColorScheme } from "@/lib/use-color-scheme";
5280
5424
  import { NAV_THEME } from "@/lib/constants";
5425
+ import { useColorScheme } from "@/lib/use-color-scheme";
5426
+ import z from "zod";
5427
+
5428
+ const signUpSchema = z.object({
5429
+ name: z
5430
+ .string()
5431
+ .trim()
5432
+ .min(1, "Name is required")
5433
+ .min(2, "Name must be at least 2 characters"),
5434
+ email: z
5435
+ .string()
5436
+ .trim()
5437
+ .min(1, "Email is required")
5438
+ .email("Enter a valid email address"),
5439
+ password: z
5440
+ .string()
5441
+ .min(1, "Password is required")
5442
+ .min(8, "Use at least 8 characters"),
5443
+ });
5444
+
5445
+ function getErrorMessage(error: unknown): string | null {
5446
+ if (!error) return null;
5447
+
5448
+ if (typeof error === "string") {
5449
+ return error;
5450
+ }
5451
+
5452
+ if (Array.isArray(error)) {
5453
+ for (const issue of error) {
5454
+ const message = getErrorMessage(issue);
5455
+ if (message) {
5456
+ return message;
5457
+ }
5458
+ }
5459
+ return null;
5460
+ }
5461
+
5462
+ if (typeof error === "object" && error !== null) {
5463
+ const maybeError = error as { message?: unknown };
5464
+ if (typeof maybeError.message === "string") {
5465
+ return maybeError.message;
5466
+ }
5467
+ }
5468
+
5469
+ return null;
5470
+ }
5281
5471
 
5282
5472
  function SignUp() {
5283
5473
  const { colorScheme } = useColorScheme();
5284
5474
  const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light;
5285
- const [name, setName] = useState("");
5286
- const [email, setEmail] = useState("");
5287
- const [password, setPassword] = useState("");
5288
- const [isLoading, setIsLoading] = useState(false);
5289
5475
  const [error, setError] = useState<string | null>(null);
5290
5476
 
5291
- async function handleSignUp() {
5292
- setIsLoading(true);
5293
- setError(null);
5294
-
5295
- await authClient.signUp.email(
5296
- {
5297
- name,
5298
- email,
5299
- password,
5300
- },
5301
- {
5302
- onError(error) {
5303
- setError(error.error?.message || "Failed to sign up");
5304
- setIsLoading(false);
5305
- },
5306
- onSuccess() {
5307
- setName("");
5308
- setEmail("");
5309
- setPassword("");
5477
+ const form = useForm({
5478
+ defaultValues: {
5479
+ name: "",
5480
+ email: "",
5481
+ password: "",
5482
+ },
5483
+ validators: {
5484
+ onSubmit: signUpSchema,
5485
+ },
5486
+ onSubmit: async ({ value, formApi }) => {
5487
+ await authClient.signUp.email(
5488
+ {
5489
+ name: value.name.trim(),
5490
+ email: value.email.trim(),
5491
+ password: value.password,
5310
5492
  },
5311
- onFinished() {
5312
- setIsLoading(false);
5493
+ {
5494
+ onError(error) {
5495
+ setError(error.error?.message || "Failed to sign up");
5496
+ },
5497
+ onSuccess() {
5498
+ setError(null);
5499
+ formApi.reset();
5500
+ },
5313
5501
  },
5314
- }
5315
- );
5316
- }
5502
+ );
5503
+ },
5504
+ });
5317
5505
 
5318
5506
  return (
5319
5507
  <View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
5320
5508
  <Text style={[styles.title, { color: theme.text }]}>Create Account</Text>
5321
5509
 
5322
- {error ? (
5323
- <View style={[styles.errorContainer, { backgroundColor: theme.notification + "20" }]}>
5324
- <Text style={[styles.errorText, { color: theme.notification }]}>{error}</Text>
5325
- </View>
5326
- ) : null}
5510
+ <form.Subscribe
5511
+ selector={(state) => ({
5512
+ isSubmitting: state.isSubmitting,
5513
+ validationError: getErrorMessage(state.errorMap.onSubmit),
5514
+ })}
5515
+ >
5516
+ {({ isSubmitting, validationError }) => {
5517
+ const formError = error ?? validationError;
5327
5518
 
5328
- <TextInput
5329
- style={[styles.input, { color: theme.text, borderColor: theme.border, backgroundColor: theme.background }]}
5330
- placeholder="Name"
5331
- placeholderTextColor={theme.text}
5332
- value={name}
5333
- onChangeText={setName}
5334
- />
5519
+ return (
5520
+ <>
5521
+ {formError ? (
5522
+ <View style={[styles.errorContainer, { backgroundColor: theme.notification + "20" }]}>
5523
+ <Text style={[styles.errorText, { color: theme.notification }]}>{formError}</Text>
5524
+ </View>
5525
+ ) : null}
5335
5526
 
5336
- <TextInput
5337
- style={[styles.input, { color: theme.text, borderColor: theme.border, backgroundColor: theme.background }]}
5338
- placeholder="Email"
5339
- placeholderTextColor={theme.text}
5340
- value={email}
5341
- onChangeText={setEmail}
5342
- keyboardType="email-address"
5343
- autoCapitalize="none"
5344
- />
5527
+ <form.Field name="name">
5528
+ {(field) => (
5529
+ <TextInput
5530
+ style={[
5531
+ styles.input,
5532
+ {
5533
+ color: theme.text,
5534
+ borderColor: theme.border,
5535
+ backgroundColor: theme.background,
5536
+ },
5537
+ ]}
5538
+ placeholder="Name"
5539
+ placeholderTextColor={theme.text}
5540
+ value={field.state.value}
5541
+ onBlur={field.handleBlur}
5542
+ onChangeText={(value) => {
5543
+ field.handleChange(value);
5544
+ if (error) {
5545
+ setError(null);
5546
+ }
5547
+ }}
5548
+ />
5549
+ )}
5550
+ </form.Field>
5345
5551
 
5346
- <TextInput
5347
- style={[styles.input, { color: theme.text, borderColor: theme.border, backgroundColor: theme.background }]}
5348
- placeholder="Password"
5349
- placeholderTextColor={theme.text}
5350
- value={password}
5351
- onChangeText={setPassword}
5352
- secureTextEntry
5353
- />
5552
+ <form.Field name="email">
5553
+ {(field) => (
5554
+ <TextInput
5555
+ style={[
5556
+ styles.input,
5557
+ {
5558
+ color: theme.text,
5559
+ borderColor: theme.border,
5560
+ backgroundColor: theme.background,
5561
+ },
5562
+ ]}
5563
+ placeholder="Email"
5564
+ placeholderTextColor={theme.text}
5565
+ value={field.state.value}
5566
+ onBlur={field.handleBlur}
5567
+ onChangeText={(value) => {
5568
+ field.handleChange(value);
5569
+ if (error) {
5570
+ setError(null);
5571
+ }
5572
+ }}
5573
+ keyboardType="email-address"
5574
+ autoCapitalize="none"
5575
+ />
5576
+ )}
5577
+ </form.Field>
5354
5578
 
5355
- <TouchableOpacity
5356
- onPress={handleSignUp}
5357
- disabled={isLoading}
5358
- style={[styles.button, { backgroundColor: theme.primary, opacity: isLoading ? 0.5 : 1 }]}
5359
- >
5360
- {isLoading ? (
5361
- <ActivityIndicator size="small" color="#ffffff" />
5362
- ) : (
5363
- <Text style={styles.buttonText}>Sign Up</Text>
5364
- )}
5365
- </TouchableOpacity>
5579
+ <form.Field name="password">
5580
+ {(field) => (
5581
+ <TextInput
5582
+ style={[
5583
+ styles.input,
5584
+ {
5585
+ color: theme.text,
5586
+ borderColor: theme.border,
5587
+ backgroundColor: theme.background,
5588
+ },
5589
+ ]}
5590
+ placeholder="Password"
5591
+ placeholderTextColor={theme.text}
5592
+ value={field.state.value}
5593
+ onBlur={field.handleBlur}
5594
+ onChangeText={(value) => {
5595
+ field.handleChange(value);
5596
+ if (error) {
5597
+ setError(null);
5598
+ }
5599
+ }}
5600
+ secureTextEntry
5601
+ onSubmitEditing={form.handleSubmit}
5602
+ />
5603
+ )}
5604
+ </form.Field>
5605
+
5606
+ <TouchableOpacity
5607
+ onPress={form.handleSubmit}
5608
+ disabled={isSubmitting}
5609
+ style={[
5610
+ styles.button,
5611
+ {
5612
+ backgroundColor: theme.primary,
5613
+ opacity: isSubmitting ? 0.5 : 1,
5614
+ },
5615
+ ]}
5616
+ >
5617
+ {isSubmitting ? (
5618
+ <ActivityIndicator size="small" color="#ffffff" />
5619
+ ) : (
5620
+ <Text style={styles.buttonText}>Sign Up</Text>
5621
+ )}
5622
+ </TouchableOpacity>
5623
+ </>
5624
+ );
5625
+ }}
5626
+ </form.Subscribe>
5366
5627
  </View>
5367
5628
  );
5368
5629
  }
@@ -5403,7 +5664,6 @@ const styles = StyleSheet.create({
5403
5664
  });
5404
5665
 
5405
5666
  export { SignUp };
5406
-
5407
5667
  `],
5408
5668
  ["auth/better-auth/convex/native/base/lib/auth-client.ts.hbs", `import { createAuthClient } from "better-auth/react";
5409
5669
  import { convexClient } from "@convex-dev/better-auth/client/plugins";
@@ -5425,6 +5685,7 @@ export const authClient = createAuthClient({
5425
5685
  });
5426
5686
  `],
5427
5687
  ["auth/better-auth/convex/native/unistyles/components/sign-in.tsx.hbs", `import { authClient } from "@/lib/auth-client";
5688
+ import { useForm } from "@tanstack/react-form";
5428
5689
  import { useState } from "react";
5429
5690
  import {
5430
5691
  ActivityIndicator,
@@ -5434,76 +5695,151 @@ import {
5434
5695
  View,
5435
5696
  } from "react-native";
5436
5697
  import { StyleSheet } from "react-native-unistyles";
5698
+ import z from "zod";
5437
5699
 
5438
- export function SignIn() {
5439
- const [email, setEmail] = useState("");
5440
- const [password, setPassword] = useState("");
5441
- const [isLoading, setIsLoading] = useState(false);
5442
- const [error, setError] = useState<string | null>(null);
5700
+ const signInSchema = z.object({
5701
+ email: z
5702
+ .string()
5703
+ .trim()
5704
+ .min(1, "Email is required")
5705
+ .email("Enter a valid email address"),
5706
+ password: z
5707
+ .string()
5708
+ .min(1, "Password is required")
5709
+ .min(8, "Use at least 8 characters"),
5710
+ });
5443
5711
 
5444
- const handleLogin = async () => {
5445
- setIsLoading(true);
5446
- setError(null);
5712
+ function getErrorMessage(error: unknown): string | null {
5713
+ if (!error) return null;
5447
5714
 
5448
- await authClient.signIn.email(
5449
- {
5450
- email,
5451
- password,
5452
- },
5453
- {
5454
- onError: (error) => {
5455
- setError(error.error?.message || "Failed to sign in");
5456
- setIsLoading(false);
5457
- },
5458
- onSuccess: () => {
5459
- setEmail("");
5460
- setPassword("");
5715
+ if (typeof error === "string") {
5716
+ return error;
5717
+ }
5718
+
5719
+ if (Array.isArray(error)) {
5720
+ for (const issue of error) {
5721
+ const message = getErrorMessage(issue);
5722
+ if (message) {
5723
+ return message;
5724
+ }
5725
+ }
5726
+ return null;
5727
+ }
5728
+
5729
+ if (typeof error === "object" && error !== null) {
5730
+ const maybeError = error as { message?: unknown };
5731
+ if (typeof maybeError.message === "string") {
5732
+ return maybeError.message;
5733
+ }
5734
+ }
5735
+
5736
+ return null;
5737
+ }
5738
+
5739
+ export function SignIn() {
5740
+ const [error, setError] = useState<string | null>(null);
5741
+
5742
+ const form = useForm({
5743
+ defaultValues: {
5744
+ email: "",
5745
+ password: "",
5746
+ },
5747
+ validators: {
5748
+ onSubmit: signInSchema,
5749
+ },
5750
+ onSubmit: async ({ value, formApi }) => {
5751
+ await authClient.signIn.email(
5752
+ {
5753
+ email: value.email.trim(),
5754
+ password: value.password,
5461
5755
  },
5462
- onFinished: () => {
5463
- setIsLoading(false);
5756
+ {
5757
+ onError(error) {
5758
+ setError(error.error?.message || "Failed to sign in");
5759
+ },
5760
+ onSuccess() {
5761
+ setError(null);
5762
+ formApi.reset();
5763
+ },
5464
5764
  },
5465
- },
5466
- );
5467
- };
5765
+ );
5766
+ },
5767
+ });
5468
5768
 
5469
5769
  return (
5470
5770
  <View style={styles.container}>
5471
5771
  <Text style={styles.title}>Sign In</Text>
5472
5772
 
5473
- {error && (
5474
- <View style={styles.errorContainer}>
5475
- <Text style={styles.errorText}>{error}</Text>
5476
- </View>
5477
- )}
5478
-
5479
- <TextInput
5480
- style={styles.input}
5481
- placeholder="Email"
5482
- value={email}
5483
- onChangeText={setEmail}
5484
- keyboardType="email-address"
5485
- autoCapitalize="none"
5486
- />
5773
+ <form.Subscribe
5774
+ selector={(state) => ({
5775
+ isSubmitting: state.isSubmitting,
5776
+ validationError: getErrorMessage(state.errorMap.onSubmit),
5777
+ })}
5778
+ >
5779
+ {({ isSubmitting, validationError }) => {
5780
+ const formError = error ?? validationError;
5487
5781
 
5488
- <TextInput
5489
- style={styles.input}
5490
- placeholder="Password"
5491
- value={password}
5492
- onChangeText={setPassword}
5493
- secureTextEntry
5494
- />
5782
+ return (
5783
+ <>
5784
+ {formError ? (
5785
+ <View style={styles.errorContainer}>
5786
+ <Text style={styles.errorText}>{formError}</Text>
5787
+ </View>
5788
+ ) : null}
5789
+
5790
+ <form.Field name="email">
5791
+ {(field) => (
5792
+ <TextInput
5793
+ style={styles.input}
5794
+ placeholder="Email"
5795
+ value={field.state.value}
5796
+ onBlur={field.handleBlur}
5797
+ onChangeText={(value) => {
5798
+ field.handleChange(value);
5799
+ if (error) {
5800
+ setError(null);
5801
+ }
5802
+ }}
5803
+ keyboardType="email-address"
5804
+ autoCapitalize="none"
5805
+ />
5806
+ )}
5807
+ </form.Field>
5808
+
5809
+ <form.Field name="password">
5810
+ {(field) => (
5811
+ <TextInput
5812
+ style={styles.input}
5813
+ placeholder="Password"
5814
+ value={field.state.value}
5815
+ onBlur={field.handleBlur}
5816
+ onChangeText={(value) => {
5817
+ field.handleChange(value);
5818
+ if (error) {
5819
+ setError(null);
5820
+ }
5821
+ }}
5822
+ secureTextEntry
5823
+ onSubmitEditing={form.handleSubmit}
5824
+ />
5825
+ )}
5826
+ </form.Field>
5495
5827
 
5496
- <TouchableOpacity
5497
- onPress={handleLogin}
5498
- disabled={isLoading}
5499
- style={styles.button}
5500
- >
5501
- {isLoading ? (
5502
- <ActivityIndicator size="small" color="#fff" />
5503
- ) : (
5504
- <Text style={styles.buttonText}>Sign In</Text>
5505
- )}
5506
- </TouchableOpacity>
5828
+ <TouchableOpacity
5829
+ onPress={form.handleSubmit}
5830
+ disabled={isSubmitting}
5831
+ style={styles.button}
5832
+ >
5833
+ {isSubmitting ? (
5834
+ <ActivityIndicator size="small" color="#fff" />
5835
+ ) : (
5836
+ <Text style={styles.buttonText}>Sign In</Text>
5837
+ )}
5838
+ </TouchableOpacity>
5839
+ </>
5840
+ );
5841
+ }}
5842
+ </form.Subscribe>
5507
5843
  </View>
5508
5844
  );
5509
5845
  }
@@ -5553,6 +5889,7 @@ const styles = StyleSheet.create((theme) => ({
5553
5889
  }));
5554
5890
  `],
5555
5891
  ["auth/better-auth/convex/native/unistyles/components/sign-up.tsx.hbs", `import { authClient } from "@/lib/auth-client";
5892
+ import { useForm } from "@tanstack/react-form";
5556
5893
  import { useState } from "react";
5557
5894
  import {
5558
5895
  ActivityIndicator,
@@ -5562,86 +5899,175 @@ import {
5562
5899
  View,
5563
5900
  } from "react-native";
5564
5901
  import { StyleSheet } from "react-native-unistyles";
5902
+ import z from "zod";
5903
+
5904
+ const signUpSchema = z.object({
5905
+ name: z
5906
+ .string()
5907
+ .trim()
5908
+ .min(1, "Name is required")
5909
+ .min(2, "Name must be at least 2 characters"),
5910
+ email: z
5911
+ .string()
5912
+ .trim()
5913
+ .min(1, "Email is required")
5914
+ .email("Enter a valid email address"),
5915
+ password: z
5916
+ .string()
5917
+ .min(1, "Password is required")
5918
+ .min(8, "Use at least 8 characters"),
5919
+ });
5920
+
5921
+ function getErrorMessage(error: unknown): string | null {
5922
+ if (!error) return null;
5923
+
5924
+ if (typeof error === "string") {
5925
+ return error;
5926
+ }
5927
+
5928
+ if (Array.isArray(error)) {
5929
+ for (const issue of error) {
5930
+ const message = getErrorMessage(issue);
5931
+ if (message) {
5932
+ return message;
5933
+ }
5934
+ }
5935
+ return null;
5936
+ }
5937
+
5938
+ if (typeof error === "object" && error !== null) {
5939
+ const maybeError = error as { message?: unknown };
5940
+ if (typeof maybeError.message === "string") {
5941
+ return maybeError.message;
5942
+ }
5943
+ }
5944
+
5945
+ return null;
5946
+ }
5565
5947
 
5566
5948
  export function SignUp() {
5567
- const [name, setName] = useState("");
5568
- const [email, setEmail] = useState("");
5569
- const [password, setPassword] = useState("");
5570
- const [isLoading, setIsLoading] = useState(false);
5571
5949
  const [error, setError] = useState<string | null>(null);
5572
5950
 
5573
- const handleSignUp = async () => {
5574
- setIsLoading(true);
5575
- setError(null);
5576
-
5577
- await authClient.signUp.email(
5578
- {
5579
- name,
5580
- email,
5581
- password,
5582
- },
5583
- {
5584
- onError: (error) => {
5585
- setError(error.error?.message || "Failed to sign up");
5586
- setIsLoading(false);
5587
- },
5588
- onSuccess: () => {
5589
- setName("");
5590
- setEmail("");
5591
- setPassword("");
5951
+ const form = useForm({
5952
+ defaultValues: {
5953
+ name: "",
5954
+ email: "",
5955
+ password: "",
5956
+ },
5957
+ validators: {
5958
+ onSubmit: signUpSchema,
5959
+ },
5960
+ onSubmit: async ({ value, formApi }) => {
5961
+ await authClient.signUp.email(
5962
+ {
5963
+ name: value.name.trim(),
5964
+ email: value.email.trim(),
5965
+ password: value.password,
5592
5966
  },
5593
- onFinished: () => {
5594
- setIsLoading(false);
5967
+ {
5968
+ onError(error) {
5969
+ setError(error.error?.message || "Failed to sign up");
5970
+ },
5971
+ onSuccess() {
5972
+ setError(null);
5973
+ formApi.reset();
5974
+ },
5595
5975
  },
5596
- },
5597
- );
5598
- };
5976
+ );
5977
+ },
5978
+ });
5599
5979
 
5600
5980
  return (
5601
5981
  <View style={styles.container}>
5602
5982
  <Text style={styles.title}>Create Account</Text>
5603
5983
 
5604
- {error && (
5605
- <View style={styles.errorContainer}>
5606
- <Text style={styles.errorText}>{error}</Text>
5607
- </View>
5608
- )}
5609
-
5610
- <TextInput
5611
- style={styles.input}
5612
- placeholder="Name"
5613
- value={name}
5614
- onChangeText={setName}
5615
- />
5616
-
5617
- <TextInput
5618
- style={styles.input}
5619
- placeholder="Email"
5620
- value={email}
5621
- onChangeText={setEmail}
5622
- keyboardType="email-address"
5623
- autoCapitalize="none"
5624
- />
5984
+ <form.Subscribe
5985
+ selector={(state) => ({
5986
+ isSubmitting: state.isSubmitting,
5987
+ validationError: getErrorMessage(state.errorMap.onSubmit),
5988
+ })}
5989
+ >
5990
+ {({ isSubmitting, validationError }) => {
5991
+ const formError = error ?? validationError;
5625
5992
 
5626
- <TextInput
5627
- style={styles.inputLast}
5628
- placeholder="Password"
5629
- value={password}
5630
- onChangeText={setPassword}
5631
- secureTextEntry
5632
- />
5993
+ return (
5994
+ <>
5995
+ {formError ? (
5996
+ <View style={styles.errorContainer}>
5997
+ <Text style={styles.errorText}>{formError}</Text>
5998
+ </View>
5999
+ ) : null}
6000
+
6001
+ <form.Field name="name">
6002
+ {(field) => (
6003
+ <TextInput
6004
+ style={styles.input}
6005
+ placeholder="Name"
6006
+ value={field.state.value}
6007
+ onBlur={field.handleBlur}
6008
+ onChangeText={(value) => {
6009
+ field.handleChange(value);
6010
+ if (error) {
6011
+ setError(null);
6012
+ }
6013
+ }}
6014
+ />
6015
+ )}
6016
+ </form.Field>
6017
+
6018
+ <form.Field name="email">
6019
+ {(field) => (
6020
+ <TextInput
6021
+ style={styles.input}
6022
+ placeholder="Email"
6023
+ value={field.state.value}
6024
+ onBlur={field.handleBlur}
6025
+ onChangeText={(value) => {
6026
+ field.handleChange(value);
6027
+ if (error) {
6028
+ setError(null);
6029
+ }
6030
+ }}
6031
+ keyboardType="email-address"
6032
+ autoCapitalize="none"
6033
+ />
6034
+ )}
6035
+ </form.Field>
6036
+
6037
+ <form.Field name="password">
6038
+ {(field) => (
6039
+ <TextInput
6040
+ style={styles.inputLast}
6041
+ placeholder="Password"
6042
+ value={field.state.value}
6043
+ onBlur={field.handleBlur}
6044
+ onChangeText={(value) => {
6045
+ field.handleChange(value);
6046
+ if (error) {
6047
+ setError(null);
6048
+ }
6049
+ }}
6050
+ secureTextEntry
6051
+ onSubmitEditing={form.handleSubmit}
6052
+ />
6053
+ )}
6054
+ </form.Field>
5633
6055
 
5634
- <TouchableOpacity
5635
- onPress={handleSignUp}
5636
- disabled={isLoading}
5637
- style={styles.button}
5638
- >
5639
- {isLoading ? (
5640
- <ActivityIndicator size="small" color="#fff" />
5641
- ) : (
5642
- <Text style={styles.buttonText}>Sign Up</Text>
5643
- )}
5644
- </TouchableOpacity>
6056
+ <TouchableOpacity
6057
+ onPress={form.handleSubmit}
6058
+ disabled={isSubmitting}
6059
+ style={styles.button}
6060
+ >
6061
+ {isSubmitting ? (
6062
+ <ActivityIndicator size="small" color="#fff" />
6063
+ ) : (
6064
+ <Text style={styles.buttonText}>Sign Up</Text>
6065
+ )}
6066
+ </TouchableOpacity>
6067
+ </>
6068
+ );
6069
+ }}
6070
+ </form.Subscribe>
5645
6071
  </View>
5646
6072
  );
5647
6073
  }
@@ -5699,161 +6125,351 @@ const styles = StyleSheet.create((theme) => ({
5699
6125
  }));
5700
6126
  `],
5701
6127
  ["auth/better-auth/convex/native/uniwind/components/sign-in.tsx.hbs", `import { authClient } from "@/lib/auth-client";
5702
- import { useState } from "react";
5703
- import { Text, View } from "react-native";
5704
- import { Button, ErrorView, Spinner, Surface, TextField } from "heroui-native";
6128
+ import { useForm } from "@tanstack/react-form";
6129
+ import { useRef } from "react";
6130
+ import { Text, TextInput, View } from "react-native";
6131
+ import { Button, FieldError, Input, Label, Spinner, Surface, TextField, useToast } from "heroui-native";
6132
+ import z from "zod";
5705
6133
 
5706
- export function SignIn() {
5707
- const [email, setEmail] = useState("");
5708
- const [password, setPassword] = useState("");
5709
- const [isLoading, setIsLoading] = useState(false);
5710
- const [error, setError] = useState<string | null>(null);
6134
+ const signInSchema = z.object({
6135
+ email: z
6136
+ .string()
6137
+ .trim()
6138
+ .min(1, "Email is required")
6139
+ .email("Enter a valid email address"),
6140
+ password: z
6141
+ .string()
6142
+ .min(1, "Password is required")
6143
+ .min(8, "Use at least 8 characters"),
6144
+ });
5711
6145
 
5712
- const handleLogin = async () => {
5713
- setIsLoading(true);
5714
- setError(null);
6146
+ function getErrorMessage(error: unknown): string | null {
6147
+ if (!error) return null;
5715
6148
 
5716
- await authClient.signIn.email(
5717
- {
5718
- email,
5719
- password,
5720
- },
5721
- {
5722
- onError: (error) => {
5723
- setError(error.error?.message || "Failed to sign in");
5724
- setIsLoading(false);
5725
- },
5726
- onSuccess: () => {
5727
- setEmail("");
5728
- setPassword("");
6149
+ if (typeof error === "string") {
6150
+ return error;
6151
+ }
6152
+
6153
+ if (Array.isArray(error)) {
6154
+ for (const issue of error) {
6155
+ const message = getErrorMessage(issue);
6156
+ if (message) {
6157
+ return message;
6158
+ }
6159
+ }
6160
+ return null;
6161
+ }
6162
+
6163
+ if (typeof error === "object" && error !== null) {
6164
+ const maybeError = error as { message?: unknown };
6165
+ if (typeof maybeError.message === "string") {
6166
+ return maybeError.message;
6167
+ }
6168
+ }
6169
+
6170
+ return null;
6171
+ }
6172
+
6173
+ export function SignIn() {
6174
+ const passwordInputRef = useRef<TextInput>(null);
6175
+ const { toast } = useToast();
6176
+
6177
+ const form = useForm({
6178
+ defaultValues: {
6179
+ email: "",
6180
+ password: "",
6181
+ },
6182
+ validators: {
6183
+ onSubmit: signInSchema,
6184
+ },
6185
+ onSubmit: async ({ value, formApi }) => {
6186
+ await authClient.signIn.email(
6187
+ {
6188
+ email: value.email.trim(),
6189
+ password: value.password,
5729
6190
  },
5730
- onFinished: () => {
5731
- setIsLoading(false);
6191
+ {
6192
+ onError(error) {
6193
+ toast.show({
6194
+ variant: "danger",
6195
+ label: error.error?.message || "Failed to sign in",
6196
+ });
6197
+ },
6198
+ onSuccess() {
6199
+ formApi.reset();
6200
+ toast.show({
6201
+ variant: "success",
6202
+ label: "Signed in successfully",
6203
+ });
6204
+ },
5732
6205
  },
5733
- },
5734
- );
5735
- };
6206
+ );
6207
+ },
6208
+ });
5736
6209
 
5737
6210
  return (
5738
6211
  <Surface variant="secondary" className="p-4 rounded-lg">
5739
6212
  <Text className="text-foreground font-medium mb-4">Sign In</Text>
5740
6213
 
5741
- <ErrorView isInvalid={!!error} className="mb-3">
5742
- {error}
5743
- </ErrorView>
5744
-
5745
- <View className="gap-3">
5746
- <TextField>
5747
- <TextField.Label>Email</TextField.Label>
5748
- <TextField.Input
5749
- value={email}
5750
- onChangeText={setEmail}
5751
- placeholder="email@example.com"
5752
- keyboardType="email-address"
5753
- autoCapitalize="none"
5754
- />
5755
- </TextField>
5756
-
5757
- <TextField>
5758
- <TextField.Label>Password</TextField.Label>
5759
- <TextField.Input
5760
- value={password}
5761
- onChangeText={setPassword}
5762
- placeholder="••••••••"
5763
- secureTextEntry
5764
- />
5765
- </TextField>
6214
+ <form.Subscribe
6215
+ selector={(state) => ({
6216
+ isSubmitting: state.isSubmitting,
6217
+ validationError: getErrorMessage(state.errorMap.onSubmit),
6218
+ })}
6219
+ >
6220
+ {({ isSubmitting, validationError }) => {
6221
+ const formError = validationError;
5766
6222
 
5767
- <Button onPress={handleLogin} isDisabled={isLoading} className="mt-1">
5768
- {isLoading ? <Spinner size="sm" color="default" /> : <Button.Label>Sign In</Button.Label>}
5769
- </Button>
5770
- </View>
6223
+ return (
6224
+ <>
6225
+ <FieldError isInvalid={!!formError} className="mb-3">
6226
+ {formError}
6227
+ </FieldError>
6228
+
6229
+ <View className="gap-3">
6230
+ <form.Field name="email">
6231
+ {(field) => (
6232
+ <TextField>
6233
+ <Label>Email</Label>
6234
+ <Input
6235
+ value={field.state.value}
6236
+ onBlur={field.handleBlur}
6237
+ onChangeText={field.handleChange}
6238
+ placeholder="email@example.com"
6239
+ keyboardType="email-address"
6240
+ autoCapitalize="none"
6241
+ autoComplete="email"
6242
+ textContentType="emailAddress"
6243
+ returnKeyType="next"
6244
+ blurOnSubmit={false}
6245
+ onSubmitEditing={() => {
6246
+ passwordInputRef.current?.focus();
6247
+ }}
6248
+ />
6249
+ </TextField>
6250
+ )}
6251
+ </form.Field>
6252
+
6253
+ <form.Field name="password">
6254
+ {(field) => (
6255
+ <TextField>
6256
+ <Label>Password</Label>
6257
+ <Input
6258
+ ref={passwordInputRef}
6259
+ value={field.state.value}
6260
+ onBlur={field.handleBlur}
6261
+ onChangeText={field.handleChange}
6262
+ placeholder="••••••••"
6263
+ secureTextEntry
6264
+ autoComplete="password"
6265
+ textContentType="password"
6266
+ returnKeyType="go"
6267
+ onSubmitEditing={form.handleSubmit}
6268
+ />
6269
+ </TextField>
6270
+ )}
6271
+ </form.Field>
6272
+
6273
+ <Button onPress={form.handleSubmit} isDisabled={isSubmitting} className="mt-1">
6274
+ {isSubmitting ? <Spinner size="sm" color="default" /> : <Button.Label>Sign In</Button.Label>}
6275
+ </Button>
6276
+ </View>
6277
+ </>
6278
+ );
6279
+ }}
6280
+ </form.Subscribe>
5771
6281
  </Surface>
5772
6282
  );
5773
6283
  }
5774
6284
  `],
5775
6285
  ["auth/better-auth/convex/native/uniwind/components/sign-up.tsx.hbs", `import { authClient } from "@/lib/auth-client";
5776
- import { useState } from "react";
5777
- import { Text, View } from "react-native";
5778
- import { Button, ErrorView, Spinner, Surface, TextField } from "heroui-native";
6286
+ import { useForm } from "@tanstack/react-form";
6287
+ import { useRef } from "react";
6288
+ import { Text, TextInput, View } from "react-native";
6289
+ import { Button, FieldError, Input, Label, Spinner, Surface, TextField, useToast } from "heroui-native";
6290
+ import z from "zod";
5779
6291
 
5780
- export function SignUp() {
5781
- const [name, setName] = useState("");
5782
- const [email, setEmail] = useState("");
5783
- const [password, setPassword] = useState("");
5784
- const [isLoading, setIsLoading] = useState(false);
5785
- const [error, setError] = useState<string | null>(null);
6292
+ const signUpSchema = z.object({
6293
+ name: z
6294
+ .string()
6295
+ .trim()
6296
+ .min(1, "Name is required")
6297
+ .min(2, "Name must be at least 2 characters"),
6298
+ email: z
6299
+ .string()
6300
+ .trim()
6301
+ .min(1, "Email is required")
6302
+ .email("Enter a valid email address"),
6303
+ password: z
6304
+ .string()
6305
+ .min(1, "Password is required")
6306
+ .min(8, "Use at least 8 characters"),
6307
+ });
5786
6308
 
5787
- const handleSignUp = async () => {
5788
- setIsLoading(true);
5789
- setError(null);
6309
+ function getErrorMessage(error: unknown): string | null {
6310
+ if (!error) return null;
5790
6311
 
5791
- await authClient.signUp.email(
5792
- {
5793
- name,
5794
- email,
5795
- password,
5796
- },
5797
- {
5798
- onError: (error) => {
5799
- setError(error.error?.message || "Failed to sign up");
5800
- setIsLoading(false);
5801
- },
5802
- onSuccess: () => {
5803
- setName("");
5804
- setEmail("");
5805
- setPassword("");
6312
+ if (typeof error === "string") {
6313
+ return error;
6314
+ }
6315
+
6316
+ if (Array.isArray(error)) {
6317
+ for (const issue of error) {
6318
+ const message = getErrorMessage(issue);
6319
+ if (message) {
6320
+ return message;
6321
+ }
6322
+ }
6323
+ return null;
6324
+ }
6325
+
6326
+ if (typeof error === "object" && error !== null) {
6327
+ const maybeError = error as { message?: unknown };
6328
+ if (typeof maybeError.message === "string") {
6329
+ return maybeError.message;
6330
+ }
6331
+ }
6332
+
6333
+ return null;
6334
+ }
6335
+
6336
+ export function SignUp() {
6337
+ const emailInputRef = useRef<TextInput>(null);
6338
+ const passwordInputRef = useRef<TextInput>(null);
6339
+ const { toast } = useToast();
6340
+
6341
+ const form = useForm({
6342
+ defaultValues: {
6343
+ name: "",
6344
+ email: "",
6345
+ password: "",
6346
+ },
6347
+ validators: {
6348
+ onSubmit: signUpSchema,
6349
+ },
6350
+ onSubmit: async ({ value, formApi }) => {
6351
+ await authClient.signUp.email(
6352
+ {
6353
+ name: value.name.trim(),
6354
+ email: value.email.trim(),
6355
+ password: value.password,
5806
6356
  },
5807
- onFinished: () => {
5808
- setIsLoading(false);
6357
+ {
6358
+ onError(error) {
6359
+ toast.show({
6360
+ variant: "danger",
6361
+ label: error.error?.message || "Failed to sign up",
6362
+ });
6363
+ },
6364
+ onSuccess() {
6365
+ formApi.reset();
6366
+ toast.show({
6367
+ variant: "success",
6368
+ label: "Account created successfully",
6369
+ });
6370
+ },
5809
6371
  },
5810
- },
5811
- );
5812
- };
6372
+ );
6373
+ },
6374
+ });
5813
6375
 
5814
6376
  return (
5815
6377
  <Surface variant="secondary" className="p-4 rounded-lg">
5816
6378
  <Text className="text-foreground font-medium mb-4">Create Account</Text>
5817
6379
 
5818
- <ErrorView isInvalid={!!error} className="mb-3">
5819
- {error}
5820
- </ErrorView>
5821
-
5822
- <View className="gap-3">
5823
- <TextField>
5824
- <TextField.Label>Name</TextField.Label>
5825
- <TextField.Input value={name} onChangeText={setName} placeholder="John Doe" />
5826
- </TextField>
5827
-
5828
- <TextField>
5829
- <TextField.Label>Email</TextField.Label>
5830
- <TextField.Input
5831
- value={email}
5832
- onChangeText={setEmail}
5833
- placeholder="email@example.com"
5834
- keyboardType="email-address"
5835
- autoCapitalize="none"
5836
- />
5837
- </TextField>
5838
-
5839
- <TextField>
5840
- <TextField.Label>Password</TextField.Label>
5841
- <TextField.Input
5842
- value={password}
5843
- onChangeText={setPassword}
5844
- placeholder="••••••••"
5845
- secureTextEntry
5846
- />
5847
- </TextField>
6380
+ <form.Subscribe
6381
+ selector={(state) => ({
6382
+ isSubmitting: state.isSubmitting,
6383
+ validationError: getErrorMessage(state.errorMap.onSubmit),
6384
+ })}
6385
+ >
6386
+ {({ isSubmitting, validationError }) => {
6387
+ const formError = validationError;
5848
6388
 
5849
- <Button onPress={handleSignUp} isDisabled={isLoading} className="mt-1">
5850
- {isLoading ? (
5851
- <Spinner size="sm" color="default" />
5852
- ) : (
5853
- <Button.Label>Create Account</Button.Label>
5854
- )}
5855
- </Button>
5856
- </View>
6389
+ return (
6390
+ <>
6391
+ <FieldError isInvalid={!!formError} className="mb-3">
6392
+ {formError}
6393
+ </FieldError>
6394
+
6395
+ <View className="gap-3">
6396
+ <form.Field name="name">
6397
+ {(field) => (
6398
+ <TextField>
6399
+ <Label>Name</Label>
6400
+ <Input
6401
+ value={field.state.value}
6402
+ onBlur={field.handleBlur}
6403
+ onChangeText={field.handleChange}
6404
+ placeholder="John Doe"
6405
+ autoComplete="name"
6406
+ textContentType="name"
6407
+ returnKeyType="next"
6408
+ blurOnSubmit={false}
6409
+ onSubmitEditing={() => {
6410
+ emailInputRef.current?.focus();
6411
+ }}
6412
+ />
6413
+ </TextField>
6414
+ )}
6415
+ </form.Field>
6416
+
6417
+ <form.Field name="email">
6418
+ {(field) => (
6419
+ <TextField>
6420
+ <Label>Email</Label>
6421
+ <Input
6422
+ ref={emailInputRef}
6423
+ value={field.state.value}
6424
+ onBlur={field.handleBlur}
6425
+ onChangeText={field.handleChange}
6426
+ placeholder="email@example.com"
6427
+ keyboardType="email-address"
6428
+ autoCapitalize="none"
6429
+ autoComplete="email"
6430
+ textContentType="emailAddress"
6431
+ returnKeyType="next"
6432
+ blurOnSubmit={false}
6433
+ onSubmitEditing={() => {
6434
+ passwordInputRef.current?.focus();
6435
+ }}
6436
+ />
6437
+ </TextField>
6438
+ )}
6439
+ </form.Field>
6440
+
6441
+ <form.Field name="password">
6442
+ {(field) => (
6443
+ <TextField>
6444
+ <Label>Password</Label>
6445
+ <Input
6446
+ ref={passwordInputRef}
6447
+ value={field.state.value}
6448
+ onBlur={field.handleBlur}
6449
+ onChangeText={field.handleChange}
6450
+ placeholder="••••••••"
6451
+ secureTextEntry
6452
+ autoComplete="new-password"
6453
+ textContentType="newPassword"
6454
+ returnKeyType="go"
6455
+ onSubmitEditing={form.handleSubmit}
6456
+ />
6457
+ </TextField>
6458
+ )}
6459
+ </form.Field>
6460
+
6461
+ <Button onPress={form.handleSubmit} isDisabled={isSubmitting} className="mt-1">
6462
+ {isSubmitting ? (
6463
+ <Spinner size="sm" color="default" />
6464
+ ) : (
6465
+ <Button.Label>Create Account</Button.Label>
6466
+ )}
6467
+ </Button>
6468
+ </View>
6469
+ </>
6470
+ );
6471
+ }}
6472
+ </form.Subscribe>
5857
6473
  </Surface>
5858
6474
  );
5859
6475
  }
@@ -7333,130 +7949,234 @@ import { queryClient } from "@/utils/trpc";
7333
7949
  {{#if (eq api "orpc")}}
7334
7950
  import { queryClient } from "@/utils/orpc";
7335
7951
  {{/if}}
7952
+ import { useForm } from "@tanstack/react-form";
7336
7953
  import { useState } from "react";
7337
7954
  import {
7338
- ActivityIndicator,
7339
- Text,
7340
- TextInput,
7341
- TouchableOpacity,
7342
- View,
7343
- StyleSheet,
7955
+ ActivityIndicator,
7956
+ StyleSheet,
7957
+ Text,
7958
+ TextInput,
7959
+ TouchableOpacity,
7960
+ View,
7344
7961
  } from "react-native";
7345
- import { useColorScheme } from "@/lib/use-color-scheme";
7346
7962
  import { NAV_THEME } from "@/lib/constants";
7963
+ import { useColorScheme } from "@/lib/use-color-scheme";
7964
+ import z from "zod";
7347
7965
 
7348
- function SignIn() {
7349
- const { colorScheme } = useColorScheme();
7350
- const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light;
7351
- const [form, setForm] = useState({ email: "", password: "" });
7352
- const [isLoading, setIsLoading] = useState(false);
7353
- const [error, setError] = useState<string | null>(null);
7966
+ const signInSchema = z.object({
7967
+ email: z
7968
+ .string()
7969
+ .trim()
7970
+ .min(1, "Email is required")
7971
+ .email("Enter a valid email address"),
7972
+ password: z
7973
+ .string()
7974
+ .min(1, "Password is required")
7975
+ .min(8, "Use at least 8 characters"),
7976
+ });
7977
+
7978
+ function getErrorMessage(error: unknown): string | null {
7979
+ if (!error) return null;
7354
7980
 
7355
- function handleFormChange(field: "email" | "password", value: string) {
7356
- setForm(prev => ({ ...prev, [field]: value }));
7981
+ if (typeof error === "string") {
7982
+ return error;
7983
+ }
7984
+
7985
+ if (Array.isArray(error)) {
7986
+ for (const issue of error) {
7987
+ const message = getErrorMessage(issue);
7988
+ if (message) {
7989
+ return message;
7990
+ }
7357
7991
  }
7992
+ return null;
7993
+ }
7358
7994
 
7359
- async function handleLogin() {
7360
- setIsLoading(true);
7361
- setError(null);
7995
+ if (typeof error === "object" && error !== null) {
7996
+ const maybeError = error as { message?: unknown };
7997
+ if (typeof maybeError.message === "string") {
7998
+ return maybeError.message;
7999
+ }
8000
+ }
7362
8001
 
7363
- await authClient.signIn.email(
7364
- {
7365
- email: form.email,
7366
- password: form.password,
7367
- },
7368
- {
7369
- onError(error) {
7370
- setError(error.error?.message || "Failed to sign in");
7371
- setIsLoading(false);
8002
+ return null;
8003
+ }
8004
+
8005
+ function SignIn() {
8006
+ const { colorScheme } = useColorScheme();
8007
+ const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light;
8008
+ const [error, setError] = useState<string | null>(null);
8009
+
8010
+ const form = useForm({
8011
+ defaultValues: {
8012
+ email: "",
8013
+ password: "",
7372
8014
  },
7373
- onSuccess() {
7374
- setForm({ email: "", password: "" });
7375
- {{#if (eq api "orpc")}}
7376
- queryClient.refetchQueries();
7377
- {{/if}}
7378
- {{#if (eq api "trpc")}}
7379
- queryClient.refetchQueries();
7380
- {{/if}}
8015
+ validators: {
8016
+ onSubmit: signInSchema,
7381
8017
  },
7382
- onFinished() {
7383
- setIsLoading(false);
8018
+ onSubmit: async ({ value, formApi }) => {
8019
+ await authClient.signIn.email(
8020
+ {
8021
+ email: value.email.trim(),
8022
+ password: value.password,
8023
+ },
8024
+ {
8025
+ onError(error) {
8026
+ setError(error.error?.message || "Failed to sign in");
8027
+ },
8028
+ onSuccess() {
8029
+ setError(null);
8030
+ formApi.reset();
8031
+ {{#if (eq api "orpc")}}
8032
+ queryClient.refetchQueries();
8033
+ {{/if}}
8034
+ {{#if (eq api "trpc")}}
8035
+ queryClient.refetchQueries();
8036
+ {{/if}}
8037
+ },
8038
+ },
8039
+ );
7384
8040
  },
7385
- }
7386
- );
7387
- }
8041
+ });
7388
8042
 
7389
- return (
8043
+ return (
7390
8044
  <View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
7391
- <Text style={[styles.title, { color: theme.text }]}>Sign In</Text>
8045
+ <Text style={[styles.title, { color: theme.text }]}>Sign In</Text>
7392
8046
 
7393
- {error ? (
7394
- <View style={[styles.errorContainer, { backgroundColor: theme.notification + "20" }]}>
7395
- <Text style={[styles.errorText, { color: theme.notification }]}>{error}</Text>
7396
- </View>
7397
- ) : null}
8047
+ <form.Subscribe
8048
+ selector={(state) => ({
8049
+ isSubmitting: state.isSubmitting,
8050
+ validationError: getErrorMessage(state.errorMap.onSubmit),
8051
+ })}
8052
+ >
8053
+ {({ isSubmitting, validationError }) => {
8054
+ const formError = error ?? validationError;
7398
8055
 
7399
- <TextInput style={[ styles.input, { color: theme.text, borderColor: theme.border, backgroundColor:
7400
- theme.background }, ]} placeholder="Email" placeholderTextColor={theme.text} value={form.email}
7401
- onChangeText={value=> handleFormChange("email", value)}
7402
- keyboardType="email-address"
7403
- autoCapitalize="none"
7404
- />
8056
+ return (
8057
+ <>
8058
+ {formError ? (
8059
+ <View style={[styles.errorContainer, { backgroundColor: theme.notification + "20" }]}>
8060
+ <Text style={[styles.errorText, { color: theme.notification }]}>{formError}</Text>
8061
+ </View>
8062
+ ) : null}
7405
8063
 
7406
- <TextInput style={[ styles.input, { color: theme.text, borderColor: theme.border, backgroundColor:
7407
- theme.background }, ]} placeholder="Password" placeholderTextColor={theme.text} value={form.password}
7408
- onChangeText={value=> handleFormChange("password", value)}
7409
- secureTextEntry
7410
- />
8064
+ <form.Field name="email">
8065
+ {(field) => (
8066
+ <TextInput
8067
+ style={[
8068
+ styles.input,
8069
+ {
8070
+ color: theme.text,
8071
+ borderColor: theme.border,
8072
+ backgroundColor: theme.background,
8073
+ },
8074
+ ]}
8075
+ placeholder="Email"
8076
+ placeholderTextColor={theme.text}
8077
+ value={field.state.value}
8078
+ onBlur={field.handleBlur}
8079
+ onChangeText={(value) => {
8080
+ field.handleChange(value);
8081
+ if (error) {
8082
+ setError(null);
8083
+ }
8084
+ }}
8085
+ keyboardType="email-address"
8086
+ autoCapitalize="none"
8087
+ />
8088
+ )}
8089
+ </form.Field>
7411
8090
 
7412
- <TouchableOpacity onPress={handleLogin} disabled={isLoading} style={[ styles.button, { backgroundColor:
7413
- theme.primary, opacity: isLoading ? 0.5 : 1 }, ]}>
7414
- {isLoading ? (
7415
- <ActivityIndicator size="small" color="#ffffff" />
7416
- ) : (
7417
- <Text style={styles.buttonText}>Sign In</Text>
7418
- )}
7419
- </TouchableOpacity>
8091
+ <form.Field name="password">
8092
+ {(field) => (
8093
+ <TextInput
8094
+ style={[
8095
+ styles.input,
8096
+ {
8097
+ color: theme.text,
8098
+ borderColor: theme.border,
8099
+ backgroundColor: theme.background,
8100
+ },
8101
+ ]}
8102
+ placeholder="Password"
8103
+ placeholderTextColor={theme.text}
8104
+ value={field.state.value}
8105
+ onBlur={field.handleBlur}
8106
+ onChangeText={(value) => {
8107
+ field.handleChange(value);
8108
+ if (error) {
8109
+ setError(null);
8110
+ }
8111
+ }}
8112
+ secureTextEntry
8113
+ onSubmitEditing={form.handleSubmit}
8114
+ />
8115
+ )}
8116
+ </form.Field>
8117
+
8118
+ <TouchableOpacity
8119
+ onPress={form.handleSubmit}
8120
+ disabled={isSubmitting}
8121
+ style={[
8122
+ styles.button,
8123
+ {
8124
+ backgroundColor: theme.primary,
8125
+ opacity: isSubmitting ? 0.5 : 1,
8126
+ },
8127
+ ]}
8128
+ >
8129
+ {isSubmitting ? (
8130
+ <ActivityIndicator size="small" color="#ffffff" />
8131
+ ) : (
8132
+ <Text style={styles.buttonText}>Sign In</Text>
8133
+ )}
8134
+ </TouchableOpacity>
8135
+ </>
8136
+ );
8137
+ }}
8138
+ </form.Subscribe>
7420
8139
  </View>
7421
- );
7422
- }
8140
+ );
8141
+ }
7423
8142
 
7424
- const styles = StyleSheet.create({
7425
- card: {
8143
+ const styles = StyleSheet.create({
8144
+ card: {
7426
8145
  marginTop: 16,
7427
8146
  padding: 16,
7428
8147
  borderWidth: 1,
7429
- },
7430
- title: {
8148
+ },
8149
+ title: {
7431
8150
  fontSize: 18,
7432
8151
  fontWeight: "bold",
7433
8152
  marginBottom: 12,
7434
- },
7435
- errorContainer: {
8153
+ },
8154
+ errorContainer: {
7436
8155
  marginBottom: 12,
7437
8156
  padding: 8,
7438
- },
7439
- errorText: {
8157
+ },
8158
+ errorText: {
7440
8159
  fontSize: 14,
7441
- },
7442
- input: {
8160
+ },
8161
+ input: {
7443
8162
  borderWidth: 1,
7444
8163
  padding: 12,
7445
8164
  fontSize: 16,
7446
- marginBottom: 12,
7447
- },
7448
- button: {
8165
+ marginBottom: 12,
8166
+ },
8167
+ button: {
7449
8168
  padding: 12,
7450
8169
  alignItems: "center",
7451
8170
  justifyContent: "center",
7452
- },
7453
- buttonText: {
8171
+ },
8172
+ buttonText: {
7454
8173
  color: "#ffffff",
7455
8174
  fontSize: 16,
7456
- },
7457
- });
8175
+ },
8176
+ });
7458
8177
 
7459
- export { SignIn };`],
8178
+ export { SignIn };
8179
+ `],
7460
8180
  ["auth/better-auth/native/bare/components/sign-up.tsx.hbs", `import { authClient } from "@/lib/auth-client";
7461
8181
  {{#if (eq api "trpc")}}
7462
8182
  import { queryClient } from "@/utils/trpc";
@@ -7464,108 +8184,225 @@ import { queryClient } from "@/utils/trpc";
7464
8184
  {{#if (eq api "orpc")}}
7465
8185
  import { queryClient } from "@/utils/orpc";
7466
8186
  {{/if}}
8187
+ import { useForm } from "@tanstack/react-form";
7467
8188
  import { useState } from "react";
7468
8189
  import {
7469
8190
  ActivityIndicator,
8191
+ StyleSheet,
7470
8192
  Text,
7471
8193
  TextInput,
7472
8194
  TouchableOpacity,
7473
8195
  View,
7474
- StyleSheet,
7475
8196
  } from "react-native";
7476
- import { useColorScheme } from "@/lib/use-color-scheme";
7477
8197
  import { NAV_THEME } from "@/lib/constants";
8198
+ import { useColorScheme } from "@/lib/use-color-scheme";
8199
+ import z from "zod";
8200
+
8201
+ const signUpSchema = z.object({
8202
+ name: z
8203
+ .string()
8204
+ .trim()
8205
+ .min(1, "Name is required")
8206
+ .min(2, "Name must be at least 2 characters"),
8207
+ email: z
8208
+ .string()
8209
+ .trim()
8210
+ .min(1, "Email is required")
8211
+ .email("Enter a valid email address"),
8212
+ password: z
8213
+ .string()
8214
+ .min(1, "Password is required")
8215
+ .min(8, "Use at least 8 characters"),
8216
+ });
8217
+
8218
+ function getErrorMessage(error: unknown): string | null {
8219
+ if (!error) return null;
8220
+
8221
+ if (typeof error === "string") {
8222
+ return error;
8223
+ }
8224
+
8225
+ if (Array.isArray(error)) {
8226
+ for (const issue of error) {
8227
+ const message = getErrorMessage(issue);
8228
+ if (message) {
8229
+ return message;
8230
+ }
8231
+ }
8232
+ return null;
8233
+ }
8234
+
8235
+ if (typeof error === "object" && error !== null) {
8236
+ const maybeError = error as { message?: unknown };
8237
+ if (typeof maybeError.message === "string") {
8238
+ return maybeError.message;
8239
+ }
8240
+ }
8241
+
8242
+ return null;
8243
+ }
7478
8244
 
7479
8245
  function SignUp() {
7480
8246
  const { colorScheme } = useColorScheme();
7481
8247
  const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light;
7482
- const [name, setName] = useState("");
7483
- const [email, setEmail] = useState("");
7484
- const [password, setPassword] = useState("");
7485
- const [isLoading, setIsLoading] = useState(false);
7486
8248
  const [error, setError] = useState<string | null>(null);
7487
8249
 
7488
- async function handleSignUp() {
7489
- setIsLoading(true);
7490
- setError(null);
7491
-
7492
- await authClient.signUp.email(
7493
- {
7494
- name,
7495
- email,
7496
- password,
7497
- },
7498
- {
7499
- onError(error) {
7500
- setError(error.error?.message || "Failed to sign up");
7501
- setIsLoading(false);
7502
- },
7503
- onSuccess() {
7504
- setName("");
7505
- setEmail("");
7506
- setPassword("");
7507
- {{#if (eq api "orpc")}}
7508
- queryClient.refetchQueries();
7509
- {{/if}}
7510
- {{#if (eq api "trpc")}}
7511
- queryClient.refetchQueries();
7512
- {{/if}}
8250
+ const form = useForm({
8251
+ defaultValues: {
8252
+ name: "",
8253
+ email: "",
8254
+ password: "",
8255
+ },
8256
+ validators: {
8257
+ onSubmit: signUpSchema,
8258
+ },
8259
+ onSubmit: async ({ value, formApi }) => {
8260
+ await authClient.signUp.email(
8261
+ {
8262
+ name: value.name.trim(),
8263
+ email: value.email.trim(),
8264
+ password: value.password,
7513
8265
  },
7514
- onFinished() {
7515
- setIsLoading(false);
8266
+ {
8267
+ onError(error) {
8268
+ setError(error.error?.message || "Failed to sign up");
8269
+ },
8270
+ onSuccess() {
8271
+ setError(null);
8272
+ formApi.reset();
8273
+ {{#if (eq api "orpc")}}
8274
+ queryClient.refetchQueries();
8275
+ {{/if}}
8276
+ {{#if (eq api "trpc")}}
8277
+ queryClient.refetchQueries();
8278
+ {{/if}}
8279
+ },
7516
8280
  },
7517
- }
7518
- );
7519
- }
8281
+ );
8282
+ },
8283
+ });
7520
8284
 
7521
8285
  return (
7522
8286
  <View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
7523
8287
  <Text style={[styles.title, { color: theme.text }]}>Create Account</Text>
7524
8288
 
7525
- {error ? (
7526
- <View style={[styles.errorContainer, { backgroundColor: theme.notification + "20" }]}>
7527
- <Text style={[styles.errorText, { color: theme.notification }]}>{error}</Text>
7528
- </View>
7529
- ) : null}
8289
+ <form.Subscribe
8290
+ selector={(state) => ({
8291
+ isSubmitting: state.isSubmitting,
8292
+ validationError: getErrorMessage(state.errorMap.onSubmit),
8293
+ })}
8294
+ >
8295
+ {({ isSubmitting, validationError }) => {
8296
+ const formError = error ?? validationError;
7530
8297
 
7531
- <TextInput
7532
- style={[styles.input, { color: theme.text, borderColor: theme.border, backgroundColor: theme.background }]}
7533
- placeholder="Name"
7534
- placeholderTextColor={theme.text}
7535
- value={name}
7536
- onChangeText={setName}
7537
- />
8298
+ return (
8299
+ <>
8300
+ {formError ? (
8301
+ <View style={[styles.errorContainer, { backgroundColor: theme.notification + "20" }]}>
8302
+ <Text style={[styles.errorText, { color: theme.notification }]}>{formError}</Text>
8303
+ </View>
8304
+ ) : null}
7538
8305
 
7539
- <TextInput
7540
- style={[styles.input, { color: theme.text, borderColor: theme.border, backgroundColor: theme.background }]}
7541
- placeholder="Email"
7542
- placeholderTextColor={theme.text}
7543
- value={email}
7544
- onChangeText={setEmail}
7545
- keyboardType="email-address"
7546
- autoCapitalize="none"
7547
- />
8306
+ <form.Field name="name">
8307
+ {(field) => (
8308
+ <TextInput
8309
+ style={[
8310
+ styles.input,
8311
+ {
8312
+ color: theme.text,
8313
+ borderColor: theme.border,
8314
+ backgroundColor: theme.background,
8315
+ },
8316
+ ]}
8317
+ placeholder="Name"
8318
+ placeholderTextColor={theme.text}
8319
+ value={field.state.value}
8320
+ onBlur={field.handleBlur}
8321
+ onChangeText={(value) => {
8322
+ field.handleChange(value);
8323
+ if (error) {
8324
+ setError(null);
8325
+ }
8326
+ }}
8327
+ />
8328
+ )}
8329
+ </form.Field>
7548
8330
 
7549
- <TextInput
7550
- style={[styles.input, { color: theme.text, borderColor: theme.border, backgroundColor: theme.background }]}
7551
- placeholder="Password"
7552
- placeholderTextColor={theme.text}
7553
- value={password}
7554
- onChangeText={setPassword}
7555
- secureTextEntry
7556
- />
8331
+ <form.Field name="email">
8332
+ {(field) => (
8333
+ <TextInput
8334
+ style={[
8335
+ styles.input,
8336
+ {
8337
+ color: theme.text,
8338
+ borderColor: theme.border,
8339
+ backgroundColor: theme.background,
8340
+ },
8341
+ ]}
8342
+ placeholder="Email"
8343
+ placeholderTextColor={theme.text}
8344
+ value={field.state.value}
8345
+ onBlur={field.handleBlur}
8346
+ onChangeText={(value) => {
8347
+ field.handleChange(value);
8348
+ if (error) {
8349
+ setError(null);
8350
+ }
8351
+ }}
8352
+ keyboardType="email-address"
8353
+ autoCapitalize="none"
8354
+ />
8355
+ )}
8356
+ </form.Field>
7557
8357
 
7558
- <TouchableOpacity
7559
- onPress={handleSignUp}
7560
- disabled={isLoading}
7561
- style={[styles.button, { backgroundColor: theme.primary, opacity: isLoading ? 0.5 : 1 }]}
7562
- >
7563
- {isLoading ? (
7564
- <ActivityIndicator size="small" color="#ffffff" />
7565
- ) : (
7566
- <Text style={styles.buttonText}>Sign Up</Text>
7567
- )}
7568
- </TouchableOpacity>
8358
+ <form.Field name="password">
8359
+ {(field) => (
8360
+ <TextInput
8361
+ style={[
8362
+ styles.input,
8363
+ {
8364
+ color: theme.text,
8365
+ borderColor: theme.border,
8366
+ backgroundColor: theme.background,
8367
+ },
8368
+ ]}
8369
+ placeholder="Password"
8370
+ placeholderTextColor={theme.text}
8371
+ value={field.state.value}
8372
+ onBlur={field.handleBlur}
8373
+ onChangeText={(value) => {
8374
+ field.handleChange(value);
8375
+ if (error) {
8376
+ setError(null);
8377
+ }
8378
+ }}
8379
+ secureTextEntry
8380
+ onSubmitEditing={form.handleSubmit}
8381
+ />
8382
+ )}
8383
+ </form.Field>
8384
+
8385
+ <TouchableOpacity
8386
+ onPress={form.handleSubmit}
8387
+ disabled={isSubmitting}
8388
+ style={[
8389
+ styles.button,
8390
+ {
8391
+ backgroundColor: theme.primary,
8392
+ opacity: isSubmitting ? 0.5 : 1,
8393
+ },
8394
+ ]}
8395
+ >
8396
+ {isSubmitting ? (
8397
+ <ActivityIndicator size="small" color="#ffffff" />
8398
+ ) : (
8399
+ <Text style={styles.buttonText}>Sign Up</Text>
8400
+ )}
8401
+ </TouchableOpacity>
8402
+ </>
8403
+ );
8404
+ }}
8405
+ </form.Subscribe>
7569
8406
  </View>
7570
8407
  );
7571
8408
  }
@@ -7606,7 +8443,6 @@ const styles = StyleSheet.create({
7606
8443
  });
7607
8444
 
7608
8445
  export { SignUp };
7609
-
7610
8446
  `],
7611
8447
  ["auth/better-auth/native/base/lib/auth-client.ts.hbs", `import { expoClient } from "@better-auth/expo/client";
7612
8448
  import { createAuthClient } from "better-auth/react";
@@ -7820,6 +8656,7 @@ import { queryClient } from "@/utils/trpc";
7820
8656
  {{#if (eq api "orpc")}}
7821
8657
  import { queryClient } from "@/utils/orpc";
7822
8658
  {{/if}}
8659
+ import { useForm } from "@tanstack/react-form";
7823
8660
  import { useState } from "react";
7824
8661
  import {
7825
8662
  ActivityIndicator,
@@ -7829,82 +8666,157 @@ import {
7829
8666
  View,
7830
8667
  } from "react-native";
7831
8668
  import { StyleSheet } from "react-native-unistyles";
8669
+ import z from "zod";
8670
+
8671
+ const signInSchema = z.object({
8672
+ email: z
8673
+ .string()
8674
+ .trim()
8675
+ .min(1, "Email is required")
8676
+ .email("Enter a valid email address"),
8677
+ password: z
8678
+ .string()
8679
+ .min(1, "Password is required")
8680
+ .min(8, "Use at least 8 characters"),
8681
+ });
8682
+
8683
+ function getErrorMessage(error: unknown): string | null {
8684
+ if (!error) return null;
8685
+
8686
+ if (typeof error === "string") {
8687
+ return error;
8688
+ }
8689
+
8690
+ if (Array.isArray(error)) {
8691
+ for (const issue of error) {
8692
+ const message = getErrorMessage(issue);
8693
+ if (message) {
8694
+ return message;
8695
+ }
8696
+ }
8697
+ return null;
8698
+ }
8699
+
8700
+ if (typeof error === "object" && error !== null) {
8701
+ const maybeError = error as { message?: unknown };
8702
+ if (typeof maybeError.message === "string") {
8703
+ return maybeError.message;
8704
+ }
8705
+ }
8706
+
8707
+ return null;
8708
+ }
7832
8709
 
7833
8710
  export function SignIn() {
7834
- const [email, setEmail] = useState("");
7835
- const [password, setPassword] = useState("");
7836
- const [isLoading, setIsLoading] = useState(false);
7837
8711
  const [error, setError] = useState<string | null>(null);
7838
8712
 
7839
- const handleLogin = async () => {
7840
- setIsLoading(true);
7841
- setError(null);
7842
-
7843
- await authClient.signIn.email(
7844
- {
7845
- email,
7846
- password,
7847
- },
7848
- {
7849
- onError: (error) => {
7850
- setError(error.error?.message || "Failed to sign in");
7851
- setIsLoading(false);
7852
- },
7853
- onSuccess: () => {
7854
- setEmail("");
7855
- setPassword("");
7856
- {{#if (eq api "orpc")}}
7857
- queryClient.refetchQueries();
7858
- {{/if}}
7859
- {{#if (eq api "trpc")}}
7860
- queryClient.refetchQueries();
7861
- {{/if}}
8713
+ const form = useForm({
8714
+ defaultValues: {
8715
+ email: "",
8716
+ password: "",
8717
+ },
8718
+ validators: {
8719
+ onSubmit: signInSchema,
8720
+ },
8721
+ onSubmit: async ({ value, formApi }) => {
8722
+ await authClient.signIn.email(
8723
+ {
8724
+ email: value.email.trim(),
8725
+ password: value.password,
7862
8726
  },
7863
- onFinished: () => {
7864
- setIsLoading(false);
8727
+ {
8728
+ onError(error) {
8729
+ setError(error.error?.message || "Failed to sign in");
8730
+ },
8731
+ onSuccess() {
8732
+ setError(null);
8733
+ formApi.reset();
8734
+ {{#if (eq api "orpc")}}
8735
+ queryClient.refetchQueries();
8736
+ {{/if}}
8737
+ {{#if (eq api "trpc")}}
8738
+ queryClient.refetchQueries();
8739
+ {{/if}}
8740
+ },
7865
8741
  },
7866
- },
7867
- );
7868
- };
8742
+ );
8743
+ },
8744
+ });
7869
8745
 
7870
8746
  return (
7871
8747
  <View style={styles.container}>
7872
8748
  <Text style={styles.title}>Sign In</Text>
7873
8749
 
7874
- {error && (
7875
- <View style={styles.errorContainer}>
7876
- <Text style={styles.errorText}>{error}</Text>
7877
- </View>
7878
- )}
7879
-
7880
- <TextInput
7881
- style={styles.input}
7882
- placeholder="Email"
7883
- value={email}
7884
- onChangeText={setEmail}
7885
- keyboardType="email-address"
7886
- autoCapitalize="none"
7887
- />
8750
+ <form.Subscribe
8751
+ selector={(state) => ({
8752
+ isSubmitting: state.isSubmitting,
8753
+ validationError: getErrorMessage(state.errorMap.onSubmit),
8754
+ })}
8755
+ >
8756
+ {({ isSubmitting, validationError }) => {
8757
+ const formError = error ?? validationError;
7888
8758
 
7889
- <TextInput
7890
- style={styles.input}
7891
- placeholder="Password"
7892
- value={password}
7893
- onChangeText={setPassword}
7894
- secureTextEntry
7895
- />
8759
+ return (
8760
+ <>
8761
+ {formError ? (
8762
+ <View style={styles.errorContainer}>
8763
+ <Text style={styles.errorText}>{formError}</Text>
8764
+ </View>
8765
+ ) : null}
8766
+
8767
+ <form.Field name="email">
8768
+ {(field) => (
8769
+ <TextInput
8770
+ style={styles.input}
8771
+ placeholder="Email"
8772
+ value={field.state.value}
8773
+ onBlur={field.handleBlur}
8774
+ onChangeText={(value) => {
8775
+ field.handleChange(value);
8776
+ if (error) {
8777
+ setError(null);
8778
+ }
8779
+ }}
8780
+ keyboardType="email-address"
8781
+ autoCapitalize="none"
8782
+ />
8783
+ )}
8784
+ </form.Field>
8785
+
8786
+ <form.Field name="password">
8787
+ {(field) => (
8788
+ <TextInput
8789
+ style={styles.input}
8790
+ placeholder="Password"
8791
+ value={field.state.value}
8792
+ onBlur={field.handleBlur}
8793
+ onChangeText={(value) => {
8794
+ field.handleChange(value);
8795
+ if (error) {
8796
+ setError(null);
8797
+ }
8798
+ }}
8799
+ secureTextEntry
8800
+ onSubmitEditing={form.handleSubmit}
8801
+ />
8802
+ )}
8803
+ </form.Field>
7896
8804
 
7897
- <TouchableOpacity
7898
- onPress={handleLogin}
7899
- disabled={isLoading}
7900
- style={styles.button}
7901
- >
7902
- {isLoading ? (
7903
- <ActivityIndicator size="small" color="#fff" />
7904
- ) : (
7905
- <Text style={styles.buttonText}>Sign In</Text>
7906
- )}
7907
- </TouchableOpacity>
8805
+ <TouchableOpacity
8806
+ onPress={form.handleSubmit}
8807
+ disabled={isSubmitting}
8808
+ style={styles.button}
8809
+ >
8810
+ {isSubmitting ? (
8811
+ <ActivityIndicator size="small" color="#fff" />
8812
+ ) : (
8813
+ <Text style={styles.buttonText}>Sign In</Text>
8814
+ )}
8815
+ </TouchableOpacity>
8816
+ </>
8817
+ );
8818
+ }}
8819
+ </form.Subscribe>
7908
8820
  </View>
7909
8821
  );
7910
8822
  }
@@ -7960,6 +8872,7 @@ import { queryClient } from "@/utils/trpc";
7960
8872
  {{#if (eq api "orpc")}}
7961
8873
  import { queryClient } from "@/utils/orpc";
7962
8874
  {{/if}}
8875
+ import { useForm } from "@tanstack/react-form";
7963
8876
  import { useState } from "react";
7964
8877
  import {
7965
8878
  ActivityIndicator,
@@ -7969,92 +8882,181 @@ import {
7969
8882
  View,
7970
8883
  } from "react-native";
7971
8884
  import { StyleSheet } from "react-native-unistyles";
8885
+ import z from "zod";
8886
+
8887
+ const signUpSchema = z.object({
8888
+ name: z
8889
+ .string()
8890
+ .trim()
8891
+ .min(1, "Name is required")
8892
+ .min(2, "Name must be at least 2 characters"),
8893
+ email: z
8894
+ .string()
8895
+ .trim()
8896
+ .min(1, "Email is required")
8897
+ .email("Enter a valid email address"),
8898
+ password: z
8899
+ .string()
8900
+ .min(1, "Password is required")
8901
+ .min(8, "Use at least 8 characters"),
8902
+ });
8903
+
8904
+ function getErrorMessage(error: unknown): string | null {
8905
+ if (!error) return null;
8906
+
8907
+ if (typeof error === "string") {
8908
+ return error;
8909
+ }
8910
+
8911
+ if (Array.isArray(error)) {
8912
+ for (const issue of error) {
8913
+ const message = getErrorMessage(issue);
8914
+ if (message) {
8915
+ return message;
8916
+ }
8917
+ }
8918
+ return null;
8919
+ }
8920
+
8921
+ if (typeof error === "object" && error !== null) {
8922
+ const maybeError = error as { message?: unknown };
8923
+ if (typeof maybeError.message === "string") {
8924
+ return maybeError.message;
8925
+ }
8926
+ }
8927
+
8928
+ return null;
8929
+ }
7972
8930
 
7973
8931
  export function SignUp() {
7974
- const [name, setName] = useState("");
7975
- const [email, setEmail] = useState("");
7976
- const [password, setPassword] = useState("");
7977
- const [isLoading, setIsLoading] = useState(false);
7978
8932
  const [error, setError] = useState<string | null>(null);
7979
8933
 
7980
- const handleSignUp = async () => {
7981
- setIsLoading(true);
7982
- setError(null);
7983
-
7984
- await authClient.signUp.email(
7985
- {
7986
- name,
7987
- email,
7988
- password,
7989
- },
7990
- {
7991
- onError: (error) => {
7992
- setError(error.error?.message || "Failed to sign up");
7993
- setIsLoading(false);
7994
- },
7995
- onSuccess: () => {
7996
- setName("");
7997
- setEmail("");
7998
- setPassword("");
7999
- {{#if (eq api "orpc")}}
8000
- queryClient.refetchQueries();
8001
- {{/if}}
8002
- {{#if (eq api "trpc")}}
8003
- queryClient.refetchQueries();
8004
- {{/if}}
8005
- },
8006
- onFinished: () => {
8007
- setIsLoading(false);
8934
+ const form = useForm({
8935
+ defaultValues: {
8936
+ name: "",
8937
+ email: "",
8938
+ password: "",
8939
+ },
8940
+ validators: {
8941
+ onSubmit: signUpSchema,
8942
+ },
8943
+ onSubmit: async ({ value, formApi }) => {
8944
+ await authClient.signUp.email(
8945
+ {
8946
+ name: value.name.trim(),
8947
+ email: value.email.trim(),
8948
+ password: value.password,
8008
8949
  },
8009
- },
8010
- );
8011
- };
8950
+ {
8951
+ onError(error) {
8952
+ setError(error.error?.message || "Failed to sign up");
8953
+ },
8954
+ onSuccess() {
8955
+ setError(null);
8956
+ formApi.reset();
8957
+ {{#if (eq api "orpc")}}
8958
+ queryClient.refetchQueries();
8959
+ {{/if}}
8960
+ {{#if (eq api "trpc")}}
8961
+ queryClient.refetchQueries();
8962
+ {{/if}}
8963
+ },
8964
+ },
8965
+ );
8966
+ },
8967
+ });
8012
8968
 
8013
8969
  return (
8014
8970
  <View style={styles.container}>
8015
8971
  <Text style={styles.title}>Create Account</Text>
8016
8972
 
8017
- {error && (
8018
- <View style={styles.errorContainer}>
8019
- <Text style={styles.errorText}>{error}</Text>
8020
- </View>
8021
- )}
8022
-
8023
- <TextInput
8024
- style={styles.input}
8025
- placeholder="Name"
8026
- value={name}
8027
- onChangeText={setName}
8028
- />
8029
-
8030
- <TextInput
8031
- style={styles.input}
8032
- placeholder="Email"
8033
- value={email}
8034
- onChangeText={setEmail}
8035
- keyboardType="email-address"
8036
- autoCapitalize="none"
8037
- />
8973
+ <form.Subscribe
8974
+ selector={(state) => ({
8975
+ isSubmitting: state.isSubmitting,
8976
+ validationError: getErrorMessage(state.errorMap.onSubmit),
8977
+ })}
8978
+ >
8979
+ {({ isSubmitting, validationError }) => {
8980
+ const formError = error ?? validationError;
8038
8981
 
8039
- <TextInput
8040
- style={styles.inputLast}
8041
- placeholder="Password"
8042
- value={password}
8043
- onChangeText={setPassword}
8044
- secureTextEntry
8045
- />
8982
+ return (
8983
+ <>
8984
+ {formError ? (
8985
+ <View style={styles.errorContainer}>
8986
+ <Text style={styles.errorText}>{formError}</Text>
8987
+ </View>
8988
+ ) : null}
8989
+
8990
+ <form.Field name="name">
8991
+ {(field) => (
8992
+ <TextInput
8993
+ style={styles.input}
8994
+ placeholder="Name"
8995
+ value={field.state.value}
8996
+ onBlur={field.handleBlur}
8997
+ onChangeText={(value) => {
8998
+ field.handleChange(value);
8999
+ if (error) {
9000
+ setError(null);
9001
+ }
9002
+ }}
9003
+ />
9004
+ )}
9005
+ </form.Field>
9006
+
9007
+ <form.Field name="email">
9008
+ {(field) => (
9009
+ <TextInput
9010
+ style={styles.input}
9011
+ placeholder="Email"
9012
+ value={field.state.value}
9013
+ onBlur={field.handleBlur}
9014
+ onChangeText={(value) => {
9015
+ field.handleChange(value);
9016
+ if (error) {
9017
+ setError(null);
9018
+ }
9019
+ }}
9020
+ keyboardType="email-address"
9021
+ autoCapitalize="none"
9022
+ />
9023
+ )}
9024
+ </form.Field>
9025
+
9026
+ <form.Field name="password">
9027
+ {(field) => (
9028
+ <TextInput
9029
+ style={styles.inputLast}
9030
+ placeholder="Password"
9031
+ value={field.state.value}
9032
+ onBlur={field.handleBlur}
9033
+ onChangeText={(value) => {
9034
+ field.handleChange(value);
9035
+ if (error) {
9036
+ setError(null);
9037
+ }
9038
+ }}
9039
+ secureTextEntry
9040
+ onSubmitEditing={form.handleSubmit}
9041
+ />
9042
+ )}
9043
+ </form.Field>
8046
9044
 
8047
- <TouchableOpacity
8048
- onPress={handleSignUp}
8049
- disabled={isLoading}
8050
- style={styles.button}
8051
- >
8052
- {isLoading ? (
8053
- <ActivityIndicator size="small" color="#fff" />
8054
- ) : (
8055
- <Text style={styles.buttonText}>Sign Up</Text>
8056
- )}
8057
- </TouchableOpacity>
9045
+ <TouchableOpacity
9046
+ onPress={form.handleSubmit}
9047
+ disabled={isSubmitting}
9048
+ style={styles.button}
9049
+ >
9050
+ {isSubmitting ? (
9051
+ <ActivityIndicator size="small" color="#fff" />
9052
+ ) : (
9053
+ <Text style={styles.buttonText}>Sign Up</Text>
9054
+ )}
9055
+ </TouchableOpacity>
9056
+ </>
9057
+ );
9058
+ }}
9059
+ </form.Subscribe>
8058
9060
  </View>
8059
9061
  );
8060
9062
  }
@@ -8241,86 +9243,171 @@ import { queryClient } from "@/utils/trpc";
8241
9243
  {{#if (eq api "orpc")}}
8242
9244
  import { queryClient } from "@/utils/orpc";
8243
9245
  {{/if}}
8244
- import { useState } from "react";
8245
- import { Text, View } from "react-native";
8246
- import { Button, ErrorView, Spinner, Surface, TextField } from "heroui-native";
9246
+ import { useForm } from "@tanstack/react-form";
9247
+ import { useRef } from "react";
9248
+ import { Text, TextInput, View } from "react-native";
9249
+ import { Button, FieldError, Input, Label, Spinner, Surface, TextField, useToast } from "heroui-native";
9250
+ import z from "zod";
8247
9251
 
8248
- function SignIn() {
8249
- const [email, setEmail] = useState("");
8250
- const [password, setPassword] = useState("");
8251
- const [isLoading, setIsLoading] = useState(false);
8252
- const [error, setError] = useState<string | null>(null);
9252
+ const signInSchema = z.object({
9253
+ email: z
9254
+ .string()
9255
+ .trim()
9256
+ .min(1, "Email is required")
9257
+ .email("Enter a valid email address"),
9258
+ password: z
9259
+ .string()
9260
+ .min(1, "Password is required")
9261
+ .min(8, "Use at least 8 characters"),
9262
+ });
8253
9263
 
8254
- async function handleLogin() {
8255
- setIsLoading(true);
8256
- setError(null);
9264
+ function getErrorMessage(error: unknown): string | null {
9265
+ if (!error) return null;
8257
9266
 
8258
- await authClient.signIn.email(
8259
- {
8260
- email,
8261
- password,
8262
- },
8263
- {
8264
- onError(error) {
8265
- setError(error.error?.message || "Failed to sign in");
8266
- setIsLoading(false);
8267
- },
8268
- onSuccess() {
8269
- setEmail("");
8270
- setPassword("");
8271
- {{#if (eq api "orpc")}}
8272
- queryClient.refetchQueries();
8273
- {{/if}}
8274
- {{#if (eq api "trpc")}}
8275
- queryClient.refetchQueries();
8276
- {{/if}}
8277
- },
8278
- onFinished() {
8279
- setIsLoading(false);
8280
- },
9267
+ if (typeof error === "string") {
9268
+ return error;
8281
9269
  }
8282
- );
9270
+
9271
+ if (Array.isArray(error)) {
9272
+ for (const issue of error) {
9273
+ const message = getErrorMessage(issue);
9274
+ if (message) {
9275
+ return message;
9276
+ }
9277
+ }
9278
+ return null;
9279
+ }
9280
+
9281
+ if (typeof error === "object" && error !== null) {
9282
+ const maybeError = error as { message?: unknown };
9283
+ if (typeof maybeError.message === "string") {
9284
+ return maybeError.message;
9285
+ }
8283
9286
  }
8284
9287
 
9288
+ return null;
9289
+ }
9290
+
9291
+ function SignIn() {
9292
+ const passwordInputRef = useRef<TextInput>(null);
9293
+ const { toast } = useToast();
9294
+
9295
+ const form = useForm({
9296
+ defaultValues: {
9297
+ email: "",
9298
+ password: "",
9299
+ },
9300
+ validators: {
9301
+ onSubmit: signInSchema,
9302
+ },
9303
+ onSubmit: async ({ value, formApi }) => {
9304
+ await authClient.signIn.email(
9305
+ {
9306
+ email: value.email.trim(),
9307
+ password: value.password,
9308
+ },
9309
+ {
9310
+ onError(error) {
9311
+ toast.show({
9312
+ variant: "danger",
9313
+ label: error.error?.message || "Failed to sign in",
9314
+ });
9315
+ },
9316
+ onSuccess() {
9317
+ formApi.reset();
9318
+ toast.show({
9319
+ variant: "success",
9320
+ label: "Signed in successfully",
9321
+ });
9322
+ {{#if (eq api "orpc")}}
9323
+ queryClient.refetchQueries();
9324
+ {{/if}}
9325
+ {{#if (eq api "trpc")}}
9326
+ queryClient.refetchQueries();
9327
+ {{/if}}
9328
+ },
9329
+ },
9330
+ );
9331
+ },
9332
+ });
9333
+
8285
9334
  return (
8286
- <Surface variant="secondary" className="p-4 rounded-lg">
8287
- <Text className="text-foreground font-medium mb-4">Sign In</Text>
8288
-
8289
- <ErrorView isInvalid={!!error} className="mb-3">
8290
- {error}
8291
- </ErrorView>
8292
-
8293
- <View className="gap-3">
8294
- <TextField>
8295
- <TextField.Label>Email</TextField.Label>
8296
- <TextField.Input
8297
- value={email}
8298
- onChangeText={setEmail}
8299
- placeholder="email@example.com"
8300
- keyboardType="email-address"
8301
- autoCapitalize="none"
8302
- />
8303
- </TextField>
8304
-
8305
- <TextField>
8306
- <TextField.Label>Password</TextField.Label>
8307
- <TextField.Input
8308
- value={password}
8309
- onChangeText={setPassword}
8310
- placeholder="••••••••"
8311
- secureTextEntry
8312
- />
8313
- </TextField>
9335
+ <Surface variant="secondary" className="p-4 rounded-lg">
9336
+ <Text className="text-foreground font-medium mb-4">Sign In</Text>
8314
9337
 
8315
- <Button onPress={handleLogin} isDisabled={isLoading} className="mt-1">
8316
- {isLoading ? <Spinner size="sm" color="default" /> : <Button.Label>Sign In</Button.Label>}
8317
- </Button>
8318
- </View>
8319
- </Surface>
9338
+ <form.Subscribe
9339
+ selector={(state) => ({
9340
+ isSubmitting: state.isSubmitting,
9341
+ validationError: getErrorMessage(state.errorMap.onSubmit),
9342
+ })}
9343
+ >
9344
+ {({ isSubmitting, validationError }) => {
9345
+ const formError = validationError;
9346
+
9347
+ return (
9348
+ <>
9349
+ <FieldError isInvalid={!!formError} className="mb-3">
9350
+ {formError}
9351
+ </FieldError>
9352
+
9353
+ <View className="gap-3">
9354
+ <form.Field name="email">
9355
+ {(field) => (
9356
+ <TextField>
9357
+ <Label>Email</Label>
9358
+ <Input
9359
+ value={field.state.value}
9360
+ onBlur={field.handleBlur}
9361
+ onChangeText={field.handleChange}
9362
+ placeholder="email@example.com"
9363
+ keyboardType="email-address"
9364
+ autoCapitalize="none"
9365
+ autoComplete="email"
9366
+ textContentType="emailAddress"
9367
+ returnKeyType="next"
9368
+ blurOnSubmit={false}
9369
+ onSubmitEditing={() => {
9370
+ passwordInputRef.current?.focus();
9371
+ }}
9372
+ />
9373
+ </TextField>
9374
+ )}
9375
+ </form.Field>
9376
+
9377
+ <form.Field name="password">
9378
+ {(field) => (
9379
+ <TextField>
9380
+ <Label>Password</Label>
9381
+ <Input
9382
+ ref={passwordInputRef}
9383
+ value={field.state.value}
9384
+ onBlur={field.handleBlur}
9385
+ onChangeText={field.handleChange}
9386
+ placeholder="••••••••"
9387
+ secureTextEntry
9388
+ autoComplete="password"
9389
+ textContentType="password"
9390
+ returnKeyType="go"
9391
+ onSubmitEditing={form.handleSubmit}
9392
+ />
9393
+ </TextField>
9394
+ )}
9395
+ </form.Field>
9396
+
9397
+ <Button onPress={form.handleSubmit} isDisabled={isSubmitting} className="mt-1">
9398
+ {isSubmitting ? <Spinner size="sm" color="default" /> : <Button.Label>Sign In</Button.Label>}
9399
+ </Button>
9400
+ </View>
9401
+ </>
9402
+ );
9403
+ }}
9404
+ </form.Subscribe>
9405
+ </Surface>
8320
9406
  );
8321
- }
9407
+ }
8322
9408
 
8323
- export { SignIn };`],
9409
+ export { SignIn };
9410
+ `],
8324
9411
  ["auth/better-auth/native/uniwind/components/sign-up.tsx.hbs", `import { authClient } from "@/lib/auth-client";
8325
9412
  {{#if (eq api "trpc")}}
8326
9413
  import { queryClient } from "@/utils/trpc";
@@ -8328,127 +9415,203 @@ import { queryClient } from "@/utils/trpc";
8328
9415
  {{#if (eq api "orpc")}}
8329
9416
  import { queryClient } from "@/utils/orpc";
8330
9417
  {{/if}}
8331
- import { useState } from "react";
8332
- import { Text, View } from "react-native";
8333
- import { Button, ErrorView, Spinner, Surface, TextField } from "heroui-native";
8334
-
8335
- function signUpHandler({
8336
- name,
8337
- email,
8338
- password,
8339
- setError,
8340
- setIsLoading,
8341
- setName,
8342
- setEmail,
8343
- setPassword,
8344
- }: {
8345
- name: string;
8346
- email: string;
8347
- password: string;
8348
- setError: (error: string | null) => void;
8349
- setIsLoading: (loading: boolean) => void;
8350
- setName: (name: string) => void;
8351
- setEmail: (email: string) => void;
8352
- setPassword: (password: string) => void;
8353
- }) {
8354
- setIsLoading(true);
8355
- setError(null);
8356
-
8357
- authClient.signUp.email(
8358
- {
8359
- name,
8360
- email,
8361
- password,
8362
- },
8363
- {
8364
- onError(error) {
8365
- setError(error.error?.message || "Failed to sign up");
8366
- setIsLoading(false);
8367
- },
8368
- onSuccess() {
8369
- setName("");
8370
- setEmail("");
8371
- setPassword("");
8372
- {{#if (eq api "orpc")}}
8373
- queryClient.refetchQueries();
8374
- {{/if}}
8375
- {{#if (eq api "trpc")}}
8376
- queryClient.refetchQueries();
8377
- {{/if}}
8378
- },
8379
- onFinished() {
8380
- setIsLoading(false);
8381
- },
8382
- }
8383
- );
9418
+ import { useForm } from "@tanstack/react-form";
9419
+ import { useRef } from "react";
9420
+ import { Text, TextInput, View } from "react-native";
9421
+ import { Button, FieldError, Input, Label, Spinner, Surface, TextField, useToast } from "heroui-native";
9422
+ import z from "zod";
9423
+
9424
+ const signUpSchema = z.object({
9425
+ name: z
9426
+ .string()
9427
+ .trim()
9428
+ .min(1, "Name is required")
9429
+ .min(2, "Name must be at least 2 characters"),
9430
+ email: z
9431
+ .string()
9432
+ .trim()
9433
+ .min(1, "Email is required")
9434
+ .email("Enter a valid email address"),
9435
+ password: z
9436
+ .string()
9437
+ .min(1, "Password is required")
9438
+ .min(8, "Use at least 8 characters"),
9439
+ });
9440
+
9441
+ function getErrorMessage(error: unknown): string | null {
9442
+ if (!error) return null;
9443
+
9444
+ if (typeof error === "string") {
9445
+ return error;
9446
+ }
9447
+
9448
+ if (Array.isArray(error)) {
9449
+ for (const issue of error) {
9450
+ const message = getErrorMessage(issue);
9451
+ if (message) {
9452
+ return message;
9453
+ }
9454
+ }
9455
+ return null;
9456
+ }
9457
+
9458
+ if (typeof error === "object" && error !== null) {
9459
+ const maybeError = error as { message?: unknown };
9460
+ if (typeof maybeError.message === "string") {
9461
+ return maybeError.message;
9462
+ }
9463
+ }
9464
+
9465
+ return null;
8384
9466
  }
8385
9467
 
8386
9468
  export function SignUp() {
8387
- const [name, setName] = useState("");
8388
- const [email, setEmail] = useState("");
8389
- const [password, setPassword] = useState("");
8390
- const [isLoading, setIsLoading] = useState(false);
8391
- const [error, setError] = useState<string | null>(null);
8392
-
8393
- function handlePress() {
8394
- signUpHandler({
8395
- name,
8396
- email,
8397
- password,
8398
- setError,
8399
- setIsLoading,
8400
- setName,
8401
- setEmail,
8402
- setPassword,
9469
+ const emailInputRef = useRef<TextInput>(null);
9470
+ const passwordInputRef = useRef<TextInput>(null);
9471
+ const { toast } = useToast();
9472
+
9473
+ const form = useForm({
9474
+ defaultValues: {
9475
+ name: "",
9476
+ email: "",
9477
+ password: "",
9478
+ },
9479
+ validators: {
9480
+ onSubmit: signUpSchema,
9481
+ },
9482
+ onSubmit: async ({ value, formApi }) => {
9483
+ await authClient.signUp.email(
9484
+ {
9485
+ name: value.name.trim(),
9486
+ email: value.email.trim(),
9487
+ password: value.password,
9488
+ },
9489
+ {
9490
+ onError(error) {
9491
+ toast.show({
9492
+ variant: "danger",
9493
+ label: error.error?.message || "Failed to sign up",
9494
+ });
9495
+ },
9496
+ onSuccess() {
9497
+ formApi.reset();
9498
+ toast.show({
9499
+ variant: "success",
9500
+ label: "Account created successfully",
9501
+ });
9502
+ {{#if (eq api "orpc")}}
9503
+ queryClient.refetchQueries();
9504
+ {{/if}}
9505
+ {{#if (eq api "trpc")}}
9506
+ queryClient.refetchQueries();
9507
+ {{/if}}
9508
+ },
9509
+ },
9510
+ );
9511
+ },
8403
9512
  });
8404
- }
8405
9513
 
8406
9514
  return (
8407
- <Surface variant="secondary" className="p-4 rounded-lg">
8408
- <Text className="text-foreground font-medium mb-4">Create Account</Text>
8409
-
8410
- <ErrorView isInvalid={!!error} className="mb-3">
8411
- {error}
8412
- </ErrorView>
8413
-
8414
- <View className="gap-3">
8415
- <TextField>
8416
- <TextField.Label>Name</TextField.Label>
8417
- <TextField.Input value={name} onChangeText={setName} placeholder="John Doe" />
8418
- </TextField>
8419
-
8420
- <TextField>
8421
- <TextField.Label>Email</TextField.Label>
8422
- <TextField.Input
8423
- value={email}
8424
- onChangeText={setEmail}
8425
- placeholder="email@example.com"
8426
- keyboardType="email-address"
8427
- autoCapitalize="none"
8428
- />
8429
- </TextField>
8430
-
8431
- <TextField>
8432
- <TextField.Label>Password</TextField.Label>
8433
- <TextField.Input
8434
- value={password}
8435
- onChangeText={setPassword}
8436
- placeholder="••••••••"
8437
- secureTextEntry
8438
- />
8439
- </TextField>
9515
+ <Surface variant="secondary" className="p-4 rounded-lg">
9516
+ <Text className="text-foreground font-medium mb-4">Create Account</Text>
8440
9517
 
8441
- <Button onPress={handlePress} isDisabled={isLoading} className="mt-1">
8442
- {isLoading ? (
8443
- <Spinner size="sm" color="default" />
8444
- ) : (
8445
- <Button.Label>Create Account</Button.Label>
8446
- )}
8447
- </Button>
8448
- </View>
8449
- </Surface>
9518
+ <form.Subscribe
9519
+ selector={(state) => ({
9520
+ isSubmitting: state.isSubmitting,
9521
+ validationError: getErrorMessage(state.errorMap.onSubmit),
9522
+ })}
9523
+ >
9524
+ {({ isSubmitting, validationError }) => {
9525
+ const formError = validationError;
9526
+
9527
+ return (
9528
+ <>
9529
+ <FieldError isInvalid={!!formError} className="mb-3">
9530
+ {formError}
9531
+ </FieldError>
9532
+
9533
+ <View className="gap-3">
9534
+ <form.Field name="name">
9535
+ {(field) => (
9536
+ <TextField>
9537
+ <Label>Name</Label>
9538
+ <Input
9539
+ value={field.state.value}
9540
+ onBlur={field.handleBlur}
9541
+ onChangeText={field.handleChange}
9542
+ placeholder="John Doe"
9543
+ autoComplete="name"
9544
+ textContentType="name"
9545
+ returnKeyType="next"
9546
+ blurOnSubmit={false}
9547
+ onSubmitEditing={() => {
9548
+ emailInputRef.current?.focus();
9549
+ }}
9550
+ />
9551
+ </TextField>
9552
+ )}
9553
+ </form.Field>
9554
+
9555
+ <form.Field name="email">
9556
+ {(field) => (
9557
+ <TextField>
9558
+ <Label>Email</Label>
9559
+ <Input
9560
+ ref={emailInputRef}
9561
+ value={field.state.value}
9562
+ onBlur={field.handleBlur}
9563
+ onChangeText={field.handleChange}
9564
+ placeholder="email@example.com"
9565
+ keyboardType="email-address"
9566
+ autoCapitalize="none"
9567
+ autoComplete="email"
9568
+ textContentType="emailAddress"
9569
+ returnKeyType="next"
9570
+ blurOnSubmit={false}
9571
+ onSubmitEditing={() => {
9572
+ passwordInputRef.current?.focus();
9573
+ }}
9574
+ />
9575
+ </TextField>
9576
+ )}
9577
+ </form.Field>
9578
+
9579
+ <form.Field name="password">
9580
+ {(field) => (
9581
+ <TextField>
9582
+ <Label>Password</Label>
9583
+ <Input
9584
+ ref={passwordInputRef}
9585
+ value={field.state.value}
9586
+ onBlur={field.handleBlur}
9587
+ onChangeText={field.handleChange}
9588
+ placeholder="••••••••"
9589
+ secureTextEntry
9590
+ autoComplete="new-password"
9591
+ textContentType="newPassword"
9592
+ returnKeyType="go"
9593
+ onSubmitEditing={form.handleSubmit}
9594
+ />
9595
+ </TextField>
9596
+ )}
9597
+ </form.Field>
9598
+
9599
+ <Button onPress={form.handleSubmit} isDisabled={isSubmitting} className="mt-1">
9600
+ {isSubmitting ? (
9601
+ <Spinner size="sm" color="default" />
9602
+ ) : (
9603
+ <Button.Label>Create Account</Button.Label>
9604
+ )}
9605
+ </Button>
9606
+ </View>
9607
+ </>
9608
+ );
9609
+ }}
9610
+ </form.Subscribe>
9611
+ </Surface>
8450
9612
  );
8451
- }`],
9613
+ }
9614
+ `],
8452
9615
  ["auth/better-auth/server/base/_gitignore", `# dependencies (bun install)
8453
9616
  node_modules
8454
9617
 
@@ -14561,14 +15724,8 @@ import { env } from "@{{projectName}}/env/server";
14561
15724
  import * as schema from "./schema";
14562
15725
 
14563
15726
  {{#if (eq dbSetup "neon")}}
14564
- import { neon, neonConfig } from '@neondatabase/serverless';
15727
+ import { neon } from '@neondatabase/serverless';
14565
15728
  import { drizzle } from 'drizzle-orm/neon-http';
14566
- import ws from "ws";
14567
-
14568
- neonConfig.webSocketConstructor = ws;
14569
-
14570
- // To work in edge environments (Cloudflare Workers, Vercel Edge, etc.), enable querying over fetch
14571
- // neonConfig.poolQueryViaFetch = true
14572
15729
 
14573
15730
  const sql = neon(env.DATABASE_URL);
14574
15731
  export const db = drizzle(sql, { schema });
@@ -14583,13 +15740,9 @@ export const db = drizzle(env.DATABASE_URL, { schema });
14583
15740
  import * as schema from "./schema";
14584
15741
 
14585
15742
  {{#if (eq dbSetup "neon")}}
14586
- import { neon, neonConfig } from '@neondatabase/serverless';
15743
+ import { neon } from '@neondatabase/serverless';
14587
15744
  import { drizzle } from 'drizzle-orm/neon-http';
14588
15745
  import { env } from "@{{projectName}}/env/server";
14589
- import ws from "ws";
14590
-
14591
- neonConfig.webSocketConstructor = ws;
14592
- neonConfig.poolQueryViaFetch = true;
14593
15746
 
14594
15747
  const sql = neon(env.DATABASE_URL || "");
14595
15748
  export const db = drizzle(sql, { schema });
@@ -14904,11 +16057,6 @@ import { PrismaClient } from "../prisma/generated/client";
14904
16057
  import { env } from "@{{projectName}}/env/server";
14905
16058
  {{#if (eq dbSetup "neon")}}
14906
16059
  import { PrismaNeon } from "@prisma/adapter-neon";
14907
- import { neonConfig } from "@neondatabase/serverless";
14908
- import ws from "ws";
14909
-
14910
- neonConfig.webSocketConstructor = ws;
14911
- neonConfig.poolQueryViaFetch = true;
14912
16060
 
14913
16061
  const adapter = new PrismaNeon({
14914
16062
  connectionString: env.DATABASE_URL,
@@ -16392,7 +17540,7 @@ import {
16392
17540
  } from "@convex-dev/agent/react";
16393
17541
  import { api } from "@{{projectName}}/backend/convex/_generated/api";
16394
17542
  import { useMutation } from "convex/react";
16395
- import { Button, Divider, Spinner, Surface, TextField, useThemeColor } from "heroui-native";
17543
+ import { Button, Separator, Spinner, Surface, Input, TextField, useThemeColor } from "heroui-native";
16396
17544
  import { useRef, useEffect, useState } from "react";
16397
17545
  import {
16398
17546
  View,
@@ -16465,7 +17613,7 @@ export default function AIScreen() {
16465
17613
  };
16466
17614
 
16467
17615
  return (
16468
- <Container>
17616
+ <Container isScrollable={false}>
16469
17617
  <KeyboardAvoidingView
16470
17618
  className="flex-1"
16471
17619
  behavior={Platform.OS === "ios" ? "padding" : "height"}
@@ -16480,19 +17628,21 @@ export default function AIScreen() {
16480
17628
  ref={scrollViewRef}
16481
17629
  className="flex-1 mb-4"
16482
17630
  showsVerticalScrollIndicator={false}
17631
+ contentContainerStyle=\\{{ flexGrow: 1, paddingBottom: 8 }}
17632
+ keyboardShouldPersistTaps="handled"
16483
17633
  >
16484
17634
  {!messages || messages.length === 0 ? (
16485
- <View className="flex-1 justify-center items-center py-10">
17635
+ <Surface variant="secondary" className="flex-1 justify-center items-center py-8 rounded-xl">
16486
17636
  <Ionicons name="chatbubble-ellipses-outline" size={32} color={mutedColor} />
16487
17637
  <Text className="text-muted text-sm mt-3">Ask me anything to get started</Text>
16488
- </View>
17638
+ </Surface>
16489
17639
  ) : (
16490
- <View className="gap-2">
17640
+ <View className="gap-3">
16491
17641
  {messages.map((message: UIMessage) => (
16492
17642
  <Surface
16493
17643
  key={message.key}
16494
17644
  variant={message.role === "user" ? "tertiary" : "secondary"}
16495
- className={\`p-3 rounded-lg \${message.role === "user" ? "ml-10" : "mr-10"}\`}
17645
+ className={\`p-3 rounded-xl \${message.role === "user" ? "ml-8" : "mr-8"}\`}
16496
17646
  >
16497
17647
  <Text className="text-xs font-medium mb-1 text-muted">
16498
17648
  {message.role === "user" ? "You" : "AI"}
@@ -16516,21 +17666,21 @@ export default function AIScreen() {
16516
17666
  )}
16517
17667
  </ScrollView>
16518
17668
 
16519
- <Divider className="mb-3" />
17669
+ <Separator className="mb-3" />
16520
17670
 
16521
17671
  <View className="flex-row items-center gap-2">
16522
17672
  <View className="flex-1">
16523
- <TextField>
16524
- <TextField.Input
16525
- value={input}
16526
- onChangeText={setInput}
16527
- placeholder="Type a message..."
16528
- onSubmitEditing={onSubmit}
16529
- editable={!isLoading}
16530
- autoFocus
16531
- />
16532
- </TextField>
16533
- </View>
17673
+ <TextField>
17674
+ <Input
17675
+ value={input}
17676
+ onChangeText={setInput}
17677
+ placeholder="Type a message..."
17678
+ onSubmitEditing={onSubmit}
17679
+ editable={!isLoading}
17680
+ returnKeyType="send"
17681
+ />
17682
+ </TextField>
17683
+ </View>
16534
17684
  <Button
16535
17685
  isIconOnly
16536
17686
  variant={input.trim() && !isLoading ? "primary" : "secondary"}
@@ -16564,7 +17714,7 @@ import { DefaultChatTransport } from "ai";
16564
17714
  import { fetch as expoFetch } from "expo/fetch";
16565
17715
  import { Ionicons } from "@expo/vector-icons";
16566
17716
  import { Container } from "@/components/container";
16567
- import { Button, Divider, ErrorView, Spinner, Surface, TextField, useThemeColor } from "heroui-native";
17717
+ import { Button, Separator, FieldError, Spinner, Surface, Input, TextField, useThemeColor } from "heroui-native";
16568
17718
  import { env } from "@{{projectName}}/env/native";
16569
17719
 
16570
17720
  const generateAPIUrl = (relativePath: string) => {
@@ -16580,7 +17730,7 @@ const generateAPIUrl = (relativePath: string) => {
16580
17730
 
16581
17731
  export default function AIScreen() {
16582
17732
  const [input, setInput] = useState("");
16583
- const { messages, error, sendMessage } = useChat({
17733
+ const { messages, error, sendMessage, status } = useChat({
16584
17734
  transport: new DefaultChatTransport({
16585
17735
  fetch: expoFetch as unknown as typeof globalThis.fetch,
16586
17736
  api: generateAPIUrl("/ai"),
@@ -16590,6 +17740,7 @@ export default function AIScreen() {
16590
17740
  const scrollViewRef = useRef<ScrollView>(null);
16591
17741
  const foregroundColor = useThemeColor("foreground");
16592
17742
  const mutedColor = useThemeColor("muted");
17743
+ const isBusy = status === "submitted" || status === "streaming";
16593
17744
 
16594
17745
  useEffect(() => {
16595
17746
  scrollViewRef.current?.scrollToEnd({ animated: true });
@@ -16597,7 +17748,7 @@ export default function AIScreen() {
16597
17748
 
16598
17749
  const onSubmit = () => {
16599
17750
  const value = input.trim();
16600
- if (value) {
17751
+ if (value && !isBusy) {
16601
17752
  sendMessage({ text: value });
16602
17753
  setInput("");
16603
17754
  }
@@ -16605,17 +17756,17 @@ export default function AIScreen() {
16605
17756
 
16606
17757
  if (error) {
16607
17758
  return (
16608
- <Container>
17759
+ <Container isScrollable={false}>
16609
17760
  <View className="flex-1 justify-center items-center px-4">
16610
17761
  <Surface variant="secondary" className="p-4 rounded-lg">
16611
- <ErrorView isInvalid>
17762
+ <FieldError isInvalid>
16612
17763
  <Text className="text-danger text-center font-medium mb-1">
16613
17764
  {error.message}
16614
17765
  </Text>
16615
17766
  <Text className="text-muted text-center text-xs">
16616
17767
  Please check your connection and try again.
16617
17768
  </Text>
16618
- </ErrorView>
17769
+ </FieldError>
16619
17770
  </Surface>
16620
17771
  </View>
16621
17772
  </Container>
@@ -16623,7 +17774,7 @@ export default function AIScreen() {
16623
17774
  }
16624
17775
 
16625
17776
  return (
16626
- <Container>
17777
+ <Container isScrollable={false}>
16627
17778
  <KeyboardAvoidingView
16628
17779
  className="flex-1"
16629
17780
  behavior={Platform.OS === "ios" ? "padding" : "height"}
@@ -16638,19 +17789,21 @@ export default function AIScreen() {
16638
17789
  ref={scrollViewRef}
16639
17790
  className="flex-1 mb-4"
16640
17791
  showsVerticalScrollIndicator={false}
17792
+ contentContainerStyle=\\{{ flexGrow: 1, paddingBottom: 8 }}
17793
+ keyboardShouldPersistTaps="handled"
16641
17794
  >
16642
17795
  {messages.length === 0 ? (
16643
- <View className="flex-1 justify-center items-center py-10">
17796
+ <Surface variant="secondary" className="flex-1 justify-center items-center py-8 rounded-xl">
16644
17797
  <Ionicons name="chatbubble-ellipses-outline" size={32} color={mutedColor} />
16645
17798
  <Text className="text-muted text-sm mt-3">Ask me anything to get started</Text>
16646
- </View>
17799
+ </Surface>
16647
17800
  ) : (
16648
- <View className="gap-2">
17801
+ <View className="gap-3">
16649
17802
  {messages.map((message) => (
16650
17803
  <Surface
16651
17804
  key={message.id}
16652
17805
  variant={message.role === "user" ? "tertiary" : "secondary"}
16653
- className={\`p-3 rounded-lg \${message.role === "user" ? "ml-10" : "mr-10"}\`}
17806
+ className={\`p-3 rounded-xl \${message.role === "user" ? "ml-8" : "mr-8"}\`}
16654
17807
  >
16655
17808
  <Text className="text-xs font-medium mb-1 text-muted">
16656
17809
  {message.role === "user" ? "You" : "AI"}
@@ -16676,35 +17829,45 @@ export default function AIScreen() {
16676
17829
  </View>
16677
17830
  </Surface>
16678
17831
  ))}
17832
+ {isBusy && (
17833
+ <Surface variant="secondary" className="p-3 mr-8 rounded-xl">
17834
+ <Text className="text-xs font-medium mb-1 text-muted">AI</Text>
17835
+ <View className="flex-row items-center gap-2">
17836
+ <Spinner size="sm" />
17837
+ <Text className="text-muted text-sm">Thinking...</Text>
17838
+ </View>
17839
+ </Surface>
17840
+ )}
16679
17841
  </View>
16680
17842
  )}
16681
17843
  </ScrollView>
16682
17844
 
16683
- <Divider className="mb-3" />
17845
+ <Separator className="mb-3" />
16684
17846
 
16685
17847
  <View className="flex-row items-center gap-2">
16686
17848
  <View className="flex-1">
16687
17849
  <TextField>
16688
- <TextField.Input
17850
+ <Input
16689
17851
  value={input}
16690
17852
  onChangeText={setInput}
16691
17853
  placeholder="Type a message..."
16692
17854
  onSubmitEditing={onSubmit}
16693
- autoFocus
17855
+ returnKeyType="send"
17856
+ editable={!isBusy}
16694
17857
  />
16695
17858
  </TextField>
16696
17859
  </View>
16697
17860
  <Button
16698
17861
  isIconOnly
16699
- variant={input.trim() ? "primary" : "secondary"}
17862
+ variant={input.trim() && !isBusy ? "primary" : "secondary"}
16700
17863
  onPress={onSubmit}
16701
- isDisabled={!input.trim()}
17864
+ isDisabled={!input.trim() || isBusy}
16702
17865
  size="sm"
16703
17866
  >
16704
17867
  <Ionicons
16705
17868
  name="arrow-up"
16706
17869
  size={18}
16707
- color={input.trim() ? foregroundColor : mutedColor}
17870
+ color={input.trim() && !isBusy ? foregroundColor : mutedColor}
16708
17871
  />
16709
17872
  </Button>
16710
17873
  </View>
@@ -18814,7 +19977,7 @@ import { Container } from "@/components/container";
18814
19977
  import { trpc } from "@/utils/trpc";
18815
19978
  {{/if}}
18816
19979
  {{/unless}}
18817
- import { Button, Checkbox, Chip, Spinner, Surface, TextField, useThemeColor } from "heroui-native";
19980
+ import { Button, Checkbox, Chip, Spinner, Surface, Input, TextField, useThemeColor } from "heroui-native";
18818
19981
 
18819
19982
  export default function TodosScreen() {
18820
19983
  const [newTodoText, setNewTodoText] = useState("");
@@ -18941,7 +20104,7 @@ export default function TodosScreen() {
18941
20104
  <View className="flex-row items-center gap-2">
18942
20105
  <View className="flex-1">
18943
20106
  <TextField>
18944
- <TextField.Input
20107
+ <Input
18945
20108
  value={newTodoText}
18946
20109
  onChangeText={setNewTodoText}
18947
20110
  placeholder="Add a new task..."
@@ -22517,7 +23680,6 @@ module.exports = config;
22517
23680
  "@react-navigation/bottom-tabs": "^7.2.0",
22518
23681
  "@react-navigation/drawer": "^7.1.1",
22519
23682
  "@react-navigation/native": "^7.0.14",
22520
- "@tanstack/react-form": "^1.0.5",
22521
23683
  "@tanstack/react-query": "^5.85.5",
22522
23684
  {{#if (includes examples "ai")}}
22523
23685
  "@stardazed/streams-text-encoding": "^1.0.2",
@@ -22551,7 +23713,6 @@ module.exports = config;
22551
23713
  },
22552
23714
  "private": true
22553
23715
  }
22554
-
22555
23716
  `],
22556
23717
  ["frontend/native/bare/tsconfig.json.hbs", `{
22557
23718
  "extends": "expo/tsconfig.base",
@@ -23579,7 +24740,6 @@ module.exports = config;
23579
24740
  "@stardazed/streams-text-encoding": "^1.0.2",
23580
24741
  "@ungap/structured-clone": "^1.3.0",
23581
24742
  {{/if}}
23582
- "@tanstack/react-form": "^1.0.5",
23583
24743
  "babel-preset-expo": "~54.0.10",
23584
24744
  "expo": "~54.0.33",
23585
24745
  "expo-constants": "~18.0.8",
@@ -24126,7 +25286,7 @@ import { api } from "@{{projectName}}/backend/convex/_generated/api";
24126
25286
  {{#unless (or (eq backend "none") (and (eq backend "convex") (eq auth "better-auth")))}}
24127
25287
  import { Ionicons } from "@expo/vector-icons";
24128
25288
  {{/unless}}
24129
- import { Button, Chip, Divider, Spinner, Surface, useThemeColor } from "heroui-native";
25289
+ import { Button, Chip, Separator, Spinner, Surface, useThemeColor } from "heroui-native";
24130
25290
 
24131
25291
  export default function Home() {
24132
25292
  {{#if (eq api "orpc")}}
@@ -24162,8 +25322,8 @@ const isLoading = healthCheck?.isLoading;
24162
25322
  {{/unless}}
24163
25323
 
24164
25324
  return (
24165
- <Container className="p-4">
24166
- <View className="py-6 mb-4">
25325
+ <Container className="px-4 pb-4">
25326
+ <View className="py-6 mb-5">
24167
25327
  <Text className="text-3xl font-semibold text-foreground tracking-tight">
24168
25328
  Better T Stack
24169
25329
  </Text>
@@ -24171,7 +25331,7 @@ return (
24171
25331
  </View>
24172
25332
 
24173
25333
  {{#unless (or (eq backend "none") (and (eq backend "convex") (eq auth "better-auth")))}}
24174
- <Surface variant="secondary" className="p-4 rounded-lg">
25334
+ <Surface variant="secondary" className="p-4 rounded-xl">
24175
25335
  <View className="flex-row items-center justify-between mb-3">
24176
25336
  <Text className="text-foreground font-medium">System Status</Text>
24177
25337
  <Chip variant="secondary" color={isConnected ? "success" : "danger" } size="sm">
@@ -24181,9 +25341,9 @@ return (
24181
25341
  </Chip>
24182
25342
  </View>
24183
25343
 
24184
- <Divider className="mb-3" />
25344
+ <Separator className="mb-3" />
24185
25345
 
24186
- <Surface variant="tertiary" className="p-3 rounded-md">
25346
+ <Surface variant="tertiary" className="p-3 rounded-lg">
24187
25347
  <View className="flex-row items-center">
24188
25348
  <View className={\`w-2 h-2 rounded-full mr-3 \${ isConnected ? "bg-success" : "bg-muted" }\`} />
24189
25349
  <View className="flex-1">
@@ -24218,7 +25378,7 @@ return (
24218
25378
 
24219
25379
  {{#if (and (eq backend "convex") (eq auth "clerk"))}}
24220
25380
  <Authenticated>
24221
- <Surface variant="secondary" className="mt-4 p-4 rounded-lg">
25381
+ <Surface variant="secondary" className="mt-5 p-4 rounded-xl">
24222
25382
  <View className="flex-row items-center justify-between">
24223
25383
  <View className="flex-1">
24224
25384
  <Text className="text-foreground font-medium">{user?.emailAddresses[0].emailAddress}</Text>
@@ -24247,14 +25407,14 @@ return (
24247
25407
 
24248
25408
  {{#if (and (eq backend "convex") (eq auth "better-auth"))}}
24249
25409
  {user ? (
24250
- <Surface variant="secondary" className="mb-4 p-4 rounded-lg">
25410
+ <Surface variant="secondary" className="mb-4 p-4 rounded-xl">
24251
25411
  <View className="flex-row items-center justify-between">
24252
25412
  <View className="flex-1">
24253
25413
  <Text className="text-foreground font-medium">{user.name}</Text>
24254
25414
  <Text className="text-muted text-xs mt-0.5">{user.email}</Text>
24255
25415
  </View>
24256
25416
  <Button
24257
- variant="destructive"
25417
+ variant="danger"
24258
25418
  size="sm"
24259
25419
  onPress={() => {
24260
25420
  authClient.signOut();
@@ -24265,7 +25425,7 @@ return (
24265
25425
  </View>
24266
25426
  </Surface>
24267
25427
  ) : null}
24268
- <Surface variant="secondary" className="p-4 rounded-lg">
25428
+ <Surface variant="secondary" className="p-4 rounded-xl">
24269
25429
  <Text className="text-foreground font-medium mb-2">API Status</Text>
24270
25430
  <View className="flex-row items-center gap-2">
24271
25431
  <View className={\`w-2 h-2 rounded-full \${healthCheck==="OK" ? "bg-success" : "bg-danger" }\`} />
@@ -24279,7 +25439,7 @@ return (
24279
25439
  </View>
24280
25440
  </Surface>
24281
25441
  {!user && (
24282
- <View className="mt-4 gap-4">
25442
+ <View className="mt-5 gap-4">
24283
25443
  <SignIn />
24284
25444
  <SignUp />
24285
25445
  </View>
@@ -24287,7 +25447,8 @@ return (
24287
25447
  {{/if}}
24288
25448
  </Container>
24289
25449
  );
24290
- }`],
25450
+ }
25451
+ `],
24291
25452
  ["frontend/native/uniwind/app/+not-found.tsx.hbs", `import { Link, Stack } from "expo-router";
24292
25453
  import { Button, Surface } from "heroui-native";
24293
25454
  import { Text, View } from "react-native";
@@ -24356,36 +25517,49 @@ export default Modal;
24356
25517
  `],
24357
25518
  ["frontend/native/uniwind/components/container.tsx.hbs", `import { cn } from "heroui-native";
24358
25519
  import { type PropsWithChildren } from "react";
24359
- import { ScrollView, View, type ViewProps } from "react-native";
25520
+ import { ScrollView, View, type ScrollViewProps, type ViewProps } from "react-native";
24360
25521
  import Animated, { type AnimatedProps } from "react-native-reanimated";
24361
25522
  import { useSafeAreaInsets } from "react-native-safe-area-context";
24362
25523
 
24363
25524
  const AnimatedView = Animated.createAnimatedComponent(View);
24364
25525
 
24365
25526
  type Props = AnimatedProps<ViewProps> & {
24366
- className?: string;
25527
+ className?: string;
25528
+ isScrollable?: boolean;
25529
+ scrollViewProps?: Omit<ScrollViewProps, "contentContainerStyle">;
24367
25530
  };
24368
25531
 
24369
25532
  export function Container({
24370
- children,
24371
- className,
24372
- ...props
25533
+ children,
25534
+ className,
25535
+ isScrollable = true,
25536
+ scrollViewProps,
25537
+ ...props
24373
25538
  }: PropsWithChildren<Props>) {
24374
- const insets = useSafeAreaInsets();
25539
+ const insets = useSafeAreaInsets();
24375
25540
 
24376
- return (
24377
- <AnimatedView
24378
- className={cn("flex-1 bg-background", className)}
24379
- style=\\{{
24380
- paddingBottom: insets.bottom,
24381
- }}
24382
- {...props}
24383
- >
24384
- <ScrollView contentContainerStyle=\\{{ flexGrow: 1 }}>
24385
- {children}
24386
- </ScrollView>
24387
- </AnimatedView>
24388
- );
25541
+ return (
25542
+ <AnimatedView
25543
+ className={cn("flex-1 bg-background", className)}
25544
+ style=\\{{
25545
+ paddingBottom: insets.bottom,
25546
+ }}
25547
+ {...props}
25548
+ >
25549
+ {isScrollable ? (
25550
+ <ScrollView
25551
+ contentContainerStyle=\\{{ flexGrow: 1 }}
25552
+ keyboardShouldPersistTaps="handled"
25553
+ contentInsetAdjustmentBehavior="automatic"
25554
+ {...scrollViewProps}
25555
+ >
25556
+ {children}
25557
+ </ScrollView>
25558
+ ) : (
25559
+ <View className="flex-1">{children}</View>
25560
+ )}
25561
+ </AnimatedView>
25562
+ );
24389
25563
  }
24390
25564
  `],
24391
25565
  ["frontend/native/uniwind/components/theme-toggle.tsx.hbs", `import { Ionicons } from '@expo/vector-icons';
@@ -24495,17 +25669,17 @@ export function useAppTheme() {
24495
25669
  `],
24496
25670
  ["frontend/native/uniwind/metro.config.js.hbs", `const { getDefaultConfig } = require("expo/metro-config");
24497
25671
  const { withUniwindConfig } = require("uniwind/metro");
25672
+ const { wrapWithReanimatedMetroConfig } = require("react-native-reanimated/metro-config");
24498
25673
 
24499
25674
  /** @type {import('expo/metro-config').MetroConfig} */
24500
25675
  const config = getDefaultConfig(__dirname);
24501
25676
 
24502
- const uniwindConfig = withUniwindConfig(config, {
25677
+ const uniwindConfig = withUniwindConfig(wrapWithReanimatedMetroConfig(config), {
24503
25678
  cssEntryFile: "./global.css",
24504
25679
  dtsFile: "./uniwind-types.d.ts",
24505
25680
  });
24506
25681
 
24507
25682
  module.exports = uniwindConfig;
24508
-
24509
25683
  `],
24510
25684
  ["frontend/native/uniwind/package.json.hbs", `{
24511
25685
  "name": "native",
@@ -24539,7 +25713,7 @@ module.exports = uniwindConfig;
24539
25713
  "expo-router": "~6.0.14",
24540
25714
  "expo-secure-store": "~15.0.7",
24541
25715
  "expo-status-bar": "~3.0.8",
24542
- "heroui-native": "^1.0.0-beta.9",
25716
+ "heroui-native": "^1.0.0-rc.1",
24543
25717
  "react": "19.1.0",
24544
25718
  "react-dom": "19.1.0",
24545
25719
  "react-native": "0.81.5",
@@ -24554,13 +25728,14 @@ module.exports = uniwindConfig;
24554
25728
  "tailwind-merge": "^3.4.0",
24555
25729
  "tailwind-variants": "^3.2.2",
24556
25730
  "tailwindcss": "^4.1.18",
24557
- "uniwind": "^1.2.2"
25731
+ "uniwind": "^1.3.0"
24558
25732
  },
24559
25733
  "devDependencies": {
24560
25734
  "@types/node": "^24.10.0",
24561
25735
  "@types/react": "~19.1.0"
24562
25736
  }
24563
- }`],
25737
+ }
25738
+ `],
24564
25739
  ["frontend/native/uniwind/tsconfig.json.hbs", `{
24565
25740
  "extends": "expo/tsconfig.base",
24566
25741
  "compilerOptions": {
@@ -24913,7 +26088,6 @@ initOpenNextCloudflareForDev();
24913
26088
  "dependencies": {
24914
26089
  "@base-ui/react": "^1.0.0",
24915
26090
  "shadcn": "^3.6.2",
24916
- "@tanstack/react-form": "^1.27.3",
24917
26091
  "class-variance-authority": "^0.7.1",
24918
26092
  "clsx": "^2.1.1",
24919
26093
  "lucide-react": "^0.546.0",
@@ -25298,7 +26472,6 @@ export function ThemeProvider({
25298
26472
  "@react-router/fs-routes": "^7.10.1",
25299
26473
  "@react-router/node": "^7.10.1",
25300
26474
  "@react-router/serve": "^7.10.1",
25301
- "@tanstack/react-form": "^1.27.3",
25302
26475
  "class-variance-authority": "^0.7.1",
25303
26476
  "clsx": "^2.1.1",
25304
26477
  "isbot": "^5.1.28",
@@ -25727,7 +26900,6 @@ export default defineConfig({
25727
26900
  "@hookform/resolvers": "^5.1.1",
25728
26901
  "@base-ui/react": "^1.0.0",
25729
26902
  "shadcn": "^3.6.2",
25730
- "@tanstack/react-form": "^1.12.3",
25731
26903
  "@tailwindcss/vite": "^4.0.15",
25732
26904
  "@tanstack/react-router": "^1.141.1",
25733
26905
  "class-variance-authority": "^0.7.1",
@@ -26128,7 +27300,6 @@ export default defineConfig({
26128
27300
  "dependencies": {
26129
27301
  "@base-ui/react": "^1.0.0",
26130
27302
  "shadcn": "^3.6.2",
26131
- "@tanstack/react-form": "^1.23.5",
26132
27303
  "@tailwindcss/vite": "^4.1.8",
26133
27304
  "@tanstack/react-query": "^5.80.6",
26134
27305
  "@tanstack/react-router": "^1.141.1",
@@ -27561,7 +28732,6 @@ dist-ssr
27561
28732
  "dependencies": {
27562
28733
  "@tailwindcss/vite": "^4.1.13",
27563
28734
  "@tanstack/router-plugin": "^1.131.44",
27564
- "@tanstack/solid-form": "^1.20.0",
27565
28735
  "@tanstack/solid-router": "^1.131.44",
27566
28736
  "lucide-solid": "^0.544.0",
27567
28737
  "solid-js": "^1.9.9",
@@ -27883,9 +29053,7 @@ vite.config.ts.timestamp-*
27883
29053
  "tailwindcss": "^4.1.12",
27884
29054
  "vite": "^7.1.2"
27885
29055
  },
27886
- "dependencies": {
27887
- "@tanstack/svelte-form": "^1.19.2"
27888
- }
29056
+ "dependencies": {}
27889
29057
  }
27890
29058
  `],
27891
29059
  ["frontend/svelte/src/app.css", `@import "tailwindcss";