@better-t-stack/template-generator 3.20.2 → 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
  }
@@ -5135,91 +5188,187 @@ export const get = query({
5135
5188
  });
5136
5189
  `],
5137
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";
5138
5192
  import { useState } from "react";
5139
5193
  import {
5140
5194
  ActivityIndicator,
5195
+ StyleSheet,
5141
5196
  Text,
5142
5197
  TextInput,
5143
5198
  TouchableOpacity,
5144
5199
  View,
5145
- StyleSheet,
5146
5200
  } from "react-native";
5147
- import { useColorScheme } from "@/lib/use-color-scheme";
5148
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
+ }
5149
5243
 
5150
5244
  function SignIn() {
5151
5245
  const { colorScheme } = useColorScheme();
5152
5246
  const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light;
5153
- const [email, setEmail] = useState("");
5154
- const [password, setPassword] = useState("");
5155
- const [isLoading, setIsLoading] = useState(false);
5156
5247
  const [error, setError] = useState<string | null>(null);
5157
5248
 
5158
- async function handleLogin() {
5159
- setIsLoading(true);
5160
- setError(null);
5161
-
5162
- await authClient.signIn.email(
5163
- {
5164
- email,
5165
- password,
5166
- },
5167
- {
5168
- onError(error) {
5169
- setError(error.error?.message || "Failed to sign in");
5170
- setIsLoading(false);
5171
- },
5172
- onSuccess() {
5173
- setEmail("");
5174
- 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,
5175
5262
  },
5176
- onFinished() {
5177
- 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
+ },
5178
5271
  },
5179
- }
5180
- );
5181
- }
5272
+ );
5273
+ },
5274
+ });
5182
5275
 
5183
5276
  return (
5184
5277
  <View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
5185
5278
  <Text style={[styles.title, { color: theme.text }]}>Sign In</Text>
5186
5279
 
5187
- {error ? (
5188
- <View style={[styles.errorContainer, { backgroundColor: theme.notification + "20" }]}>
5189
- <Text style={[styles.errorText, { color: theme.notification }]}>{error}</Text>
5190
- </View>
5191
- ) : 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;
5192
5288
 
5193
- <TextInput
5194
- style={[styles.input, { color: theme.text, borderColor: theme.border, backgroundColor: theme.background }]}
5195
- placeholder="Email"
5196
- placeholderTextColor={theme.text}
5197
- value={email}
5198
- onChangeText={setEmail}
5199
- keyboardType="email-address"
5200
- autoCapitalize="none"
5201
- />
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}
5202
5296
 
5203
- <TextInput
5204
- style={[styles.input, { color: theme.text, borderColor: theme.border, backgroundColor: theme.background }]}
5205
- placeholder="Password"
5206
- placeholderTextColor={theme.text}
5207
- value={password}
5208
- onChangeText={setPassword}
5209
- secureTextEntry
5210
- />
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>
5211
5323
 
5212
- <TouchableOpacity
5213
- onPress={handleLogin}
5214
- disabled={isLoading}
5215
- style={[styles.button, { backgroundColor: theme.primary, opacity: isLoading ? 0.5 : 1 }]}
5216
- >
5217
- {isLoading ? (
5218
- <ActivityIndicator size="small" color="#ffffff" />
5219
- ) : (
5220
- <Text style={styles.buttonText}>Sign In</Text>
5221
- )}
5222
- </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>
5223
5372
  </View>
5224
5373
  );
5225
5374
  }
@@ -5260,105 +5409,221 @@ const styles = StyleSheet.create({
5260
5409
  });
5261
5410
 
5262
5411
  export { SignIn };
5263
-
5264
5412
  `],
5265
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";
5266
5415
  import { useState } from "react";
5267
5416
  import {
5268
5417
  ActivityIndicator,
5418
+ StyleSheet,
5269
5419
  Text,
5270
5420
  TextInput,
5271
5421
  TouchableOpacity,
5272
5422
  View,
5273
- StyleSheet,
5274
5423
  } from "react-native";
5275
- import { useColorScheme } from "@/lib/use-color-scheme";
5276
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
+ }
5277
5471
 
5278
5472
  function SignUp() {
5279
5473
  const { colorScheme } = useColorScheme();
5280
5474
  const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light;
5281
- const [name, setName] = useState("");
5282
- const [email, setEmail] = useState("");
5283
- const [password, setPassword] = useState("");
5284
- const [isLoading, setIsLoading] = useState(false);
5285
5475
  const [error, setError] = useState<string | null>(null);
5286
5476
 
5287
- async function handleSignUp() {
5288
- setIsLoading(true);
5289
- setError(null);
5290
-
5291
- await authClient.signUp.email(
5292
- {
5293
- name,
5294
- email,
5295
- password,
5296
- },
5297
- {
5298
- onError(error) {
5299
- setError(error.error?.message || "Failed to sign up");
5300
- setIsLoading(false);
5301
- },
5302
- onSuccess() {
5303
- setName("");
5304
- setEmail("");
5305
- 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,
5306
5492
  },
5307
- onFinished() {
5308
- 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
+ },
5309
5501
  },
5310
- }
5311
- );
5312
- }
5502
+ );
5503
+ },
5504
+ });
5313
5505
 
5314
5506
  return (
5315
5507
  <View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
5316
5508
  <Text style={[styles.title, { color: theme.text }]}>Create Account</Text>
5317
5509
 
5318
- {error ? (
5319
- <View style={[styles.errorContainer, { backgroundColor: theme.notification + "20" }]}>
5320
- <Text style={[styles.errorText, { color: theme.notification }]}>{error}</Text>
5321
- </View>
5322
- ) : 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;
5323
5518
 
5324
- <TextInput
5325
- style={[styles.input, { color: theme.text, borderColor: theme.border, backgroundColor: theme.background }]}
5326
- placeholder="Name"
5327
- placeholderTextColor={theme.text}
5328
- value={name}
5329
- onChangeText={setName}
5330
- />
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}
5331
5526
 
5332
- <TextInput
5333
- style={[styles.input, { color: theme.text, borderColor: theme.border, backgroundColor: theme.background }]}
5334
- placeholder="Email"
5335
- placeholderTextColor={theme.text}
5336
- value={email}
5337
- onChangeText={setEmail}
5338
- keyboardType="email-address"
5339
- autoCapitalize="none"
5340
- />
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>
5341
5551
 
5342
- <TextInput
5343
- style={[styles.input, { color: theme.text, borderColor: theme.border, backgroundColor: theme.background }]}
5344
- placeholder="Password"
5345
- placeholderTextColor={theme.text}
5346
- value={password}
5347
- onChangeText={setPassword}
5348
- secureTextEntry
5349
- />
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>
5350
5578
 
5351
- <TouchableOpacity
5352
- onPress={handleSignUp}
5353
- disabled={isLoading}
5354
- style={[styles.button, { backgroundColor: theme.primary, opacity: isLoading ? 0.5 : 1 }]}
5355
- >
5356
- {isLoading ? (
5357
- <ActivityIndicator size="small" color="#ffffff" />
5358
- ) : (
5359
- <Text style={styles.buttonText}>Sign Up</Text>
5360
- )}
5361
- </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>
5362
5627
  </View>
5363
5628
  );
5364
5629
  }
@@ -5399,7 +5664,6 @@ const styles = StyleSheet.create({
5399
5664
  });
5400
5665
 
5401
5666
  export { SignUp };
5402
-
5403
5667
  `],
5404
5668
  ["auth/better-auth/convex/native/base/lib/auth-client.ts.hbs", `import { createAuthClient } from "better-auth/react";
5405
5669
  import { convexClient } from "@convex-dev/better-auth/client/plugins";
@@ -5421,6 +5685,7 @@ export const authClient = createAuthClient({
5421
5685
  });
5422
5686
  `],
5423
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";
5424
5689
  import { useState } from "react";
5425
5690
  import {
5426
5691
  ActivityIndicator,
@@ -5430,76 +5695,151 @@ import {
5430
5695
  View,
5431
5696
  } from "react-native";
5432
5697
  import { StyleSheet } from "react-native-unistyles";
5698
+ import z from "zod";
5433
5699
 
5434
- export function SignIn() {
5435
- const [email, setEmail] = useState("");
5436
- const [password, setPassword] = useState("");
5437
- const [isLoading, setIsLoading] = useState(false);
5438
- 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
+ });
5439
5711
 
5440
- const handleLogin = async () => {
5441
- setIsLoading(true);
5442
- setError(null);
5712
+ function getErrorMessage(error: unknown): string | null {
5713
+ if (!error) return null;
5443
5714
 
5444
- await authClient.signIn.email(
5445
- {
5446
- email,
5447
- password,
5448
- },
5449
- {
5450
- onError: (error) => {
5451
- setError(error.error?.message || "Failed to sign in");
5452
- setIsLoading(false);
5453
- },
5454
- onSuccess: () => {
5455
- setEmail("");
5456
- 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,
5457
5755
  },
5458
- onFinished: () => {
5459
- 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
+ },
5460
5764
  },
5461
- },
5462
- );
5463
- };
5765
+ );
5766
+ },
5767
+ });
5464
5768
 
5465
5769
  return (
5466
5770
  <View style={styles.container}>
5467
5771
  <Text style={styles.title}>Sign In</Text>
5468
5772
 
5469
- {error && (
5470
- <View style={styles.errorContainer}>
5471
- <Text style={styles.errorText}>{error}</Text>
5472
- </View>
5473
- )}
5474
-
5475
- <TextInput
5476
- style={styles.input}
5477
- placeholder="Email"
5478
- value={email}
5479
- onChangeText={setEmail}
5480
- keyboardType="email-address"
5481
- autoCapitalize="none"
5482
- />
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;
5483
5781
 
5484
- <TextInput
5485
- style={styles.input}
5486
- placeholder="Password"
5487
- value={password}
5488
- onChangeText={setPassword}
5489
- secureTextEntry
5490
- />
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>
5491
5827
 
5492
- <TouchableOpacity
5493
- onPress={handleLogin}
5494
- disabled={isLoading}
5495
- style={styles.button}
5496
- >
5497
- {isLoading ? (
5498
- <ActivityIndicator size="small" color="#fff" />
5499
- ) : (
5500
- <Text style={styles.buttonText}>Sign In</Text>
5501
- )}
5502
- </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>
5503
5843
  </View>
5504
5844
  );
5505
5845
  }
@@ -5549,6 +5889,7 @@ const styles = StyleSheet.create((theme) => ({
5549
5889
  }));
5550
5890
  `],
5551
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";
5552
5893
  import { useState } from "react";
5553
5894
  import {
5554
5895
  ActivityIndicator,
@@ -5558,86 +5899,175 @@ import {
5558
5899
  View,
5559
5900
  } from "react-native";
5560
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
+ }
5561
5947
 
5562
5948
  export function SignUp() {
5563
- const [name, setName] = useState("");
5564
- const [email, setEmail] = useState("");
5565
- const [password, setPassword] = useState("");
5566
- const [isLoading, setIsLoading] = useState(false);
5567
5949
  const [error, setError] = useState<string | null>(null);
5568
5950
 
5569
- const handleSignUp = async () => {
5570
- setIsLoading(true);
5571
- setError(null);
5572
-
5573
- await authClient.signUp.email(
5574
- {
5575
- name,
5576
- email,
5577
- password,
5578
- },
5579
- {
5580
- onError: (error) => {
5581
- setError(error.error?.message || "Failed to sign up");
5582
- setIsLoading(false);
5583
- },
5584
- onSuccess: () => {
5585
- setName("");
5586
- setEmail("");
5587
- 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,
5588
5966
  },
5589
- onFinished: () => {
5590
- 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
+ },
5591
5975
  },
5592
- },
5593
- );
5594
- };
5976
+ );
5977
+ },
5978
+ });
5595
5979
 
5596
5980
  return (
5597
5981
  <View style={styles.container}>
5598
5982
  <Text style={styles.title}>Create Account</Text>
5599
5983
 
5600
- {error && (
5601
- <View style={styles.errorContainer}>
5602
- <Text style={styles.errorText}>{error}</Text>
5603
- </View>
5604
- )}
5605
-
5606
- <TextInput
5607
- style={styles.input}
5608
- placeholder="Name"
5609
- value={name}
5610
- onChangeText={setName}
5611
- />
5612
-
5613
- <TextInput
5614
- style={styles.input}
5615
- placeholder="Email"
5616
- value={email}
5617
- onChangeText={setEmail}
5618
- keyboardType="email-address"
5619
- autoCapitalize="none"
5620
- />
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;
5621
5992
 
5622
- <TextInput
5623
- style={styles.inputLast}
5624
- placeholder="Password"
5625
- value={password}
5626
- onChangeText={setPassword}
5627
- secureTextEntry
5628
- />
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>
5629
6055
 
5630
- <TouchableOpacity
5631
- onPress={handleSignUp}
5632
- disabled={isLoading}
5633
- style={styles.button}
5634
- >
5635
- {isLoading ? (
5636
- <ActivityIndicator size="small" color="#fff" />
5637
- ) : (
5638
- <Text style={styles.buttonText}>Sign Up</Text>
5639
- )}
5640
- </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>
5641
6071
  </View>
5642
6072
  );
5643
6073
  }
@@ -5695,161 +6125,351 @@ const styles = StyleSheet.create((theme) => ({
5695
6125
  }));
5696
6126
  `],
5697
6127
  ["auth/better-auth/convex/native/uniwind/components/sign-in.tsx.hbs", `import { authClient } from "@/lib/auth-client";
5698
- import { useState } from "react";
5699
- import { Text, View } from "react-native";
5700
- 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";
5701
6133
 
5702
- export function SignIn() {
5703
- const [email, setEmail] = useState("");
5704
- const [password, setPassword] = useState("");
5705
- const [isLoading, setIsLoading] = useState(false);
5706
- 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
+ });
5707
6145
 
5708
- const handleLogin = async () => {
5709
- setIsLoading(true);
5710
- setError(null);
6146
+ function getErrorMessage(error: unknown): string | null {
6147
+ if (!error) return null;
5711
6148
 
5712
- await authClient.signIn.email(
5713
- {
5714
- email,
5715
- password,
5716
- },
5717
- {
5718
- onError: (error) => {
5719
- setError(error.error?.message || "Failed to sign in");
5720
- setIsLoading(false);
5721
- },
5722
- onSuccess: () => {
5723
- setEmail("");
5724
- 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,
5725
6190
  },
5726
- onFinished: () => {
5727
- 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
+ },
5728
6205
  },
5729
- },
5730
- );
5731
- };
6206
+ );
6207
+ },
6208
+ });
5732
6209
 
5733
6210
  return (
5734
6211
  <Surface variant="secondary" className="p-4 rounded-lg">
5735
6212
  <Text className="text-foreground font-medium mb-4">Sign In</Text>
5736
6213
 
5737
- <ErrorView isInvalid={!!error} className="mb-3">
5738
- {error}
5739
- </ErrorView>
5740
-
5741
- <View className="gap-3">
5742
- <TextField>
5743
- <TextField.Label>Email</TextField.Label>
5744
- <TextField.Input
5745
- value={email}
5746
- onChangeText={setEmail}
5747
- placeholder="email@example.com"
5748
- keyboardType="email-address"
5749
- autoCapitalize="none"
5750
- />
5751
- </TextField>
5752
-
5753
- <TextField>
5754
- <TextField.Label>Password</TextField.Label>
5755
- <TextField.Input
5756
- value={password}
5757
- onChangeText={setPassword}
5758
- placeholder="••••••••"
5759
- secureTextEntry
5760
- />
5761
- </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;
5762
6222
 
5763
- <Button onPress={handleLogin} isDisabled={isLoading} className="mt-1">
5764
- {isLoading ? <Spinner size="sm" color="default" /> : <Button.Label>Sign In</Button.Label>}
5765
- </Button>
5766
- </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>
5767
6281
  </Surface>
5768
6282
  );
5769
6283
  }
5770
6284
  `],
5771
6285
  ["auth/better-auth/convex/native/uniwind/components/sign-up.tsx.hbs", `import { authClient } from "@/lib/auth-client";
5772
- import { useState } from "react";
5773
- import { Text, View } from "react-native";
5774
- 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";
5775
6291
 
5776
- export function SignUp() {
5777
- const [name, setName] = useState("");
5778
- const [email, setEmail] = useState("");
5779
- const [password, setPassword] = useState("");
5780
- const [isLoading, setIsLoading] = useState(false);
5781
- 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
+ });
5782
6308
 
5783
- const handleSignUp = async () => {
5784
- setIsLoading(true);
5785
- setError(null);
6309
+ function getErrorMessage(error: unknown): string | null {
6310
+ if (!error) return null;
5786
6311
 
5787
- await authClient.signUp.email(
5788
- {
5789
- name,
5790
- email,
5791
- password,
5792
- },
5793
- {
5794
- onError: (error) => {
5795
- setError(error.error?.message || "Failed to sign up");
5796
- setIsLoading(false);
5797
- },
5798
- onSuccess: () => {
5799
- setName("");
5800
- setEmail("");
5801
- 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,
5802
6356
  },
5803
- onFinished: () => {
5804
- 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
+ },
5805
6371
  },
5806
- },
5807
- );
5808
- };
6372
+ );
6373
+ },
6374
+ });
5809
6375
 
5810
6376
  return (
5811
6377
  <Surface variant="secondary" className="p-4 rounded-lg">
5812
6378
  <Text className="text-foreground font-medium mb-4">Create Account</Text>
5813
6379
 
5814
- <ErrorView isInvalid={!!error} className="mb-3">
5815
- {error}
5816
- </ErrorView>
5817
-
5818
- <View className="gap-3">
5819
- <TextField>
5820
- <TextField.Label>Name</TextField.Label>
5821
- <TextField.Input value={name} onChangeText={setName} placeholder="John Doe" />
5822
- </TextField>
5823
-
5824
- <TextField>
5825
- <TextField.Label>Email</TextField.Label>
5826
- <TextField.Input
5827
- value={email}
5828
- onChangeText={setEmail}
5829
- placeholder="email@example.com"
5830
- keyboardType="email-address"
5831
- autoCapitalize="none"
5832
- />
5833
- </TextField>
5834
-
5835
- <TextField>
5836
- <TextField.Label>Password</TextField.Label>
5837
- <TextField.Input
5838
- value={password}
5839
- onChangeText={setPassword}
5840
- placeholder="••••••••"
5841
- secureTextEntry
5842
- />
5843
- </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;
5844
6388
 
5845
- <Button onPress={handleSignUp} isDisabled={isLoading} className="mt-1">
5846
- {isLoading ? (
5847
- <Spinner size="sm" color="default" />
5848
- ) : (
5849
- <Button.Label>Create Account</Button.Label>
5850
- )}
5851
- </Button>
5852
- </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>
5853
6473
  </Surface>
5854
6474
  );
5855
6475
  }
@@ -7329,130 +7949,234 @@ import { queryClient } from "@/utils/trpc";
7329
7949
  {{#if (eq api "orpc")}}
7330
7950
  import { queryClient } from "@/utils/orpc";
7331
7951
  {{/if}}
7952
+ import { useForm } from "@tanstack/react-form";
7332
7953
  import { useState } from "react";
7333
7954
  import {
7334
- ActivityIndicator,
7335
- Text,
7336
- TextInput,
7337
- TouchableOpacity,
7338
- View,
7339
- StyleSheet,
7955
+ ActivityIndicator,
7956
+ StyleSheet,
7957
+ Text,
7958
+ TextInput,
7959
+ TouchableOpacity,
7960
+ View,
7340
7961
  } from "react-native";
7341
- import { useColorScheme } from "@/lib/use-color-scheme";
7342
7962
  import { NAV_THEME } from "@/lib/constants";
7963
+ import { useColorScheme } from "@/lib/use-color-scheme";
7964
+ import z from "zod";
7343
7965
 
7344
- function SignIn() {
7345
- const { colorScheme } = useColorScheme();
7346
- const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light;
7347
- const [form, setForm] = useState({ email: "", password: "" });
7348
- const [isLoading, setIsLoading] = useState(false);
7349
- 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
+ });
7350
7977
 
7351
- function handleFormChange(field: "email" | "password", value: string) {
7352
- setForm(prev => ({ ...prev, [field]: value }));
7978
+ function getErrorMessage(error: unknown): string | null {
7979
+ if (!error) return null;
7980
+
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
+ }
7353
7991
  }
7992
+ return null;
7993
+ }
7354
7994
 
7355
- async function handleLogin() {
7356
- setIsLoading(true);
7357
- 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
+ }
7358
8001
 
7359
- await authClient.signIn.email(
7360
- {
7361
- email: form.email,
7362
- password: form.password,
7363
- },
7364
- {
7365
- onError(error) {
7366
- setError(error.error?.message || "Failed to sign in");
7367
- 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: "",
7368
8014
  },
7369
- onSuccess() {
7370
- setForm({ email: "", password: "" });
7371
- {{#if (eq api "orpc")}}
7372
- queryClient.refetchQueries();
7373
- {{/if}}
7374
- {{#if (eq api "trpc")}}
7375
- queryClient.refetchQueries();
7376
- {{/if}}
8015
+ validators: {
8016
+ onSubmit: signInSchema,
7377
8017
  },
7378
- onFinished() {
7379
- 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
+ );
7380
8040
  },
7381
- }
7382
- );
7383
- }
8041
+ });
7384
8042
 
7385
- return (
8043
+ return (
7386
8044
  <View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
7387
- <Text style={[styles.title, { color: theme.text }]}>Sign In</Text>
8045
+ <Text style={[styles.title, { color: theme.text }]}>Sign In</Text>
7388
8046
 
7389
- {error ? (
7390
- <View style={[styles.errorContainer, { backgroundColor: theme.notification + "20" }]}>
7391
- <Text style={[styles.errorText, { color: theme.notification }]}>{error}</Text>
7392
- </View>
7393
- ) : 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;
7394
8055
 
7395
- <TextInput style={[ styles.input, { color: theme.text, borderColor: theme.border, backgroundColor:
7396
- theme.background }, ]} placeholder="Email" placeholderTextColor={theme.text} value={form.email}
7397
- onChangeText={value=> handleFormChange("email", value)}
7398
- keyboardType="email-address"
7399
- autoCapitalize="none"
7400
- />
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}
7401
8063
 
7402
- <TextInput style={[ styles.input, { color: theme.text, borderColor: theme.border, backgroundColor:
7403
- theme.background }, ]} placeholder="Password" placeholderTextColor={theme.text} value={form.password}
7404
- onChangeText={value=> handleFormChange("password", value)}
7405
- secureTextEntry
7406
- />
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>
7407
8090
 
7408
- <TouchableOpacity onPress={handleLogin} disabled={isLoading} style={[ styles.button, { backgroundColor:
7409
- theme.primary, opacity: isLoading ? 0.5 : 1 }, ]}>
7410
- {isLoading ? (
7411
- <ActivityIndicator size="small" color="#ffffff" />
7412
- ) : (
7413
- <Text style={styles.buttonText}>Sign In</Text>
7414
- )}
7415
- </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>
7416
8139
  </View>
7417
- );
7418
- }
8140
+ );
8141
+ }
7419
8142
 
7420
- const styles = StyleSheet.create({
7421
- card: {
8143
+ const styles = StyleSheet.create({
8144
+ card: {
7422
8145
  marginTop: 16,
7423
8146
  padding: 16,
7424
8147
  borderWidth: 1,
7425
- },
7426
- title: {
8148
+ },
8149
+ title: {
7427
8150
  fontSize: 18,
7428
8151
  fontWeight: "bold",
7429
8152
  marginBottom: 12,
7430
- },
7431
- errorContainer: {
8153
+ },
8154
+ errorContainer: {
7432
8155
  marginBottom: 12,
7433
8156
  padding: 8,
7434
- },
7435
- errorText: {
8157
+ },
8158
+ errorText: {
7436
8159
  fontSize: 14,
7437
- },
7438
- input: {
8160
+ },
8161
+ input: {
7439
8162
  borderWidth: 1,
7440
8163
  padding: 12,
7441
8164
  fontSize: 16,
7442
8165
  marginBottom: 12,
7443
- },
7444
- button: {
8166
+ },
8167
+ button: {
7445
8168
  padding: 12,
7446
8169
  alignItems: "center",
7447
8170
  justifyContent: "center",
7448
- },
7449
- buttonText: {
8171
+ },
8172
+ buttonText: {
7450
8173
  color: "#ffffff",
7451
8174
  fontSize: 16,
7452
- },
7453
- });
8175
+ },
8176
+ });
7454
8177
 
7455
- export { SignIn };`],
8178
+ export { SignIn };
8179
+ `],
7456
8180
  ["auth/better-auth/native/bare/components/sign-up.tsx.hbs", `import { authClient } from "@/lib/auth-client";
7457
8181
  {{#if (eq api "trpc")}}
7458
8182
  import { queryClient } from "@/utils/trpc";
@@ -7460,108 +8184,225 @@ import { queryClient } from "@/utils/trpc";
7460
8184
  {{#if (eq api "orpc")}}
7461
8185
  import { queryClient } from "@/utils/orpc";
7462
8186
  {{/if}}
8187
+ import { useForm } from "@tanstack/react-form";
7463
8188
  import { useState } from "react";
7464
8189
  import {
7465
8190
  ActivityIndicator,
8191
+ StyleSheet,
7466
8192
  Text,
7467
8193
  TextInput,
7468
8194
  TouchableOpacity,
7469
8195
  View,
7470
- StyleSheet,
7471
8196
  } from "react-native";
7472
- import { useColorScheme } from "@/lib/use-color-scheme";
7473
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
+ }
7474
8244
 
7475
8245
  function SignUp() {
7476
8246
  const { colorScheme } = useColorScheme();
7477
8247
  const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light;
7478
- const [name, setName] = useState("");
7479
- const [email, setEmail] = useState("");
7480
- const [password, setPassword] = useState("");
7481
- const [isLoading, setIsLoading] = useState(false);
7482
8248
  const [error, setError] = useState<string | null>(null);
7483
8249
 
7484
- async function handleSignUp() {
7485
- setIsLoading(true);
7486
- setError(null);
7487
-
7488
- await authClient.signUp.email(
7489
- {
7490
- name,
7491
- email,
7492
- password,
7493
- },
7494
- {
7495
- onError(error) {
7496
- setError(error.error?.message || "Failed to sign up");
7497
- setIsLoading(false);
7498
- },
7499
- onSuccess() {
7500
- setName("");
7501
- setEmail("");
7502
- setPassword("");
7503
- {{#if (eq api "orpc")}}
7504
- queryClient.refetchQueries();
7505
- {{/if}}
7506
- {{#if (eq api "trpc")}}
7507
- queryClient.refetchQueries();
7508
- {{/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,
7509
8265
  },
7510
- onFinished() {
7511
- 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
+ },
7512
8280
  },
7513
- }
7514
- );
7515
- }
8281
+ );
8282
+ },
8283
+ });
7516
8284
 
7517
8285
  return (
7518
8286
  <View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
7519
8287
  <Text style={[styles.title, { color: theme.text }]}>Create Account</Text>
7520
8288
 
7521
- {error ? (
7522
- <View style={[styles.errorContainer, { backgroundColor: theme.notification + "20" }]}>
7523
- <Text style={[styles.errorText, { color: theme.notification }]}>{error}</Text>
7524
- </View>
7525
- ) : 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;
7526
8297
 
7527
- <TextInput
7528
- style={[styles.input, { color: theme.text, borderColor: theme.border, backgroundColor: theme.background }]}
7529
- placeholder="Name"
7530
- placeholderTextColor={theme.text}
7531
- value={name}
7532
- onChangeText={setName}
7533
- />
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}
7534
8305
 
7535
- <TextInput
7536
- style={[styles.input, { color: theme.text, borderColor: theme.border, backgroundColor: theme.background }]}
7537
- placeholder="Email"
7538
- placeholderTextColor={theme.text}
7539
- value={email}
7540
- onChangeText={setEmail}
7541
- keyboardType="email-address"
7542
- autoCapitalize="none"
7543
- />
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>
7544
8330
 
7545
- <TextInput
7546
- style={[styles.input, { color: theme.text, borderColor: theme.border, backgroundColor: theme.background }]}
7547
- placeholder="Password"
7548
- placeholderTextColor={theme.text}
7549
- value={password}
7550
- onChangeText={setPassword}
7551
- secureTextEntry
7552
- />
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>
7553
8357
 
7554
- <TouchableOpacity
7555
- onPress={handleSignUp}
7556
- disabled={isLoading}
7557
- style={[styles.button, { backgroundColor: theme.primary, opacity: isLoading ? 0.5 : 1 }]}
7558
- >
7559
- {isLoading ? (
7560
- <ActivityIndicator size="small" color="#ffffff" />
7561
- ) : (
7562
- <Text style={styles.buttonText}>Sign Up</Text>
7563
- )}
7564
- </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>
7565
8406
  </View>
7566
8407
  );
7567
8408
  }
@@ -7602,7 +8443,6 @@ const styles = StyleSheet.create({
7602
8443
  });
7603
8444
 
7604
8445
  export { SignUp };
7605
-
7606
8446
  `],
7607
8447
  ["auth/better-auth/native/base/lib/auth-client.ts.hbs", `import { expoClient } from "@better-auth/expo/client";
7608
8448
  import { createAuthClient } from "better-auth/react";
@@ -7816,6 +8656,7 @@ import { queryClient } from "@/utils/trpc";
7816
8656
  {{#if (eq api "orpc")}}
7817
8657
  import { queryClient } from "@/utils/orpc";
7818
8658
  {{/if}}
8659
+ import { useForm } from "@tanstack/react-form";
7819
8660
  import { useState } from "react";
7820
8661
  import {
7821
8662
  ActivityIndicator,
@@ -7825,82 +8666,157 @@ import {
7825
8666
  View,
7826
8667
  } from "react-native";
7827
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
+ }
7828
8709
 
7829
8710
  export function SignIn() {
7830
- const [email, setEmail] = useState("");
7831
- const [password, setPassword] = useState("");
7832
- const [isLoading, setIsLoading] = useState(false);
7833
8711
  const [error, setError] = useState<string | null>(null);
7834
8712
 
7835
- const handleLogin = async () => {
7836
- setIsLoading(true);
7837
- setError(null);
7838
-
7839
- await authClient.signIn.email(
7840
- {
7841
- email,
7842
- password,
7843
- },
7844
- {
7845
- onError: (error) => {
7846
- setError(error.error?.message || "Failed to sign in");
7847
- setIsLoading(false);
7848
- },
7849
- onSuccess: () => {
7850
- setEmail("");
7851
- setPassword("");
7852
- {{#if (eq api "orpc")}}
7853
- queryClient.refetchQueries();
7854
- {{/if}}
7855
- {{#if (eq api "trpc")}}
7856
- queryClient.refetchQueries();
7857
- {{/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,
7858
8726
  },
7859
- onFinished: () => {
7860
- 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
+ },
7861
8741
  },
7862
- },
7863
- );
7864
- };
8742
+ );
8743
+ },
8744
+ });
7865
8745
 
7866
8746
  return (
7867
8747
  <View style={styles.container}>
7868
8748
  <Text style={styles.title}>Sign In</Text>
7869
8749
 
7870
- {error && (
7871
- <View style={styles.errorContainer}>
7872
- <Text style={styles.errorText}>{error}</Text>
7873
- </View>
7874
- )}
7875
-
7876
- <TextInput
7877
- style={styles.input}
7878
- placeholder="Email"
7879
- value={email}
7880
- onChangeText={setEmail}
7881
- keyboardType="email-address"
7882
- autoCapitalize="none"
7883
- />
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;
7884
8758
 
7885
- <TextInput
7886
- style={styles.input}
7887
- placeholder="Password"
7888
- value={password}
7889
- onChangeText={setPassword}
7890
- secureTextEntry
7891
- />
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>
7892
8804
 
7893
- <TouchableOpacity
7894
- onPress={handleLogin}
7895
- disabled={isLoading}
7896
- style={styles.button}
7897
- >
7898
- {isLoading ? (
7899
- <ActivityIndicator size="small" color="#fff" />
7900
- ) : (
7901
- <Text style={styles.buttonText}>Sign In</Text>
7902
- )}
7903
- </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>
7904
8820
  </View>
7905
8821
  );
7906
8822
  }
@@ -7956,6 +8872,7 @@ import { queryClient } from "@/utils/trpc";
7956
8872
  {{#if (eq api "orpc")}}
7957
8873
  import { queryClient } from "@/utils/orpc";
7958
8874
  {{/if}}
8875
+ import { useForm } from "@tanstack/react-form";
7959
8876
  import { useState } from "react";
7960
8877
  import {
7961
8878
  ActivityIndicator,
@@ -7965,92 +8882,181 @@ import {
7965
8882
  View,
7966
8883
  } from "react-native";
7967
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
+ }
7968
8930
 
7969
8931
  export function SignUp() {
7970
- const [name, setName] = useState("");
7971
- const [email, setEmail] = useState("");
7972
- const [password, setPassword] = useState("");
7973
- const [isLoading, setIsLoading] = useState(false);
7974
8932
  const [error, setError] = useState<string | null>(null);
7975
8933
 
7976
- const handleSignUp = async () => {
7977
- setIsLoading(true);
7978
- setError(null);
7979
-
7980
- await authClient.signUp.email(
7981
- {
7982
- name,
7983
- email,
7984
- password,
7985
- },
7986
- {
7987
- onError: (error) => {
7988
- setError(error.error?.message || "Failed to sign up");
7989
- setIsLoading(false);
7990
- },
7991
- onSuccess: () => {
7992
- setName("");
7993
- setEmail("");
7994
- setPassword("");
7995
- {{#if (eq api "orpc")}}
7996
- queryClient.refetchQueries();
7997
- {{/if}}
7998
- {{#if (eq api "trpc")}}
7999
- queryClient.refetchQueries();
8000
- {{/if}}
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,
8001
8949
  },
8002
- onFinished: () => {
8003
- setIsLoading(false);
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
+ },
8004
8964
  },
8005
- },
8006
- );
8007
- };
8965
+ );
8966
+ },
8967
+ });
8008
8968
 
8009
8969
  return (
8010
8970
  <View style={styles.container}>
8011
8971
  <Text style={styles.title}>Create Account</Text>
8012
8972
 
8013
- {error && (
8014
- <View style={styles.errorContainer}>
8015
- <Text style={styles.errorText}>{error}</Text>
8016
- </View>
8017
- )}
8018
-
8019
- <TextInput
8020
- style={styles.input}
8021
- placeholder="Name"
8022
- value={name}
8023
- onChangeText={setName}
8024
- />
8025
-
8026
- <TextInput
8027
- style={styles.input}
8028
- placeholder="Email"
8029
- value={email}
8030
- onChangeText={setEmail}
8031
- keyboardType="email-address"
8032
- autoCapitalize="none"
8033
- />
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;
8034
8981
 
8035
- <TextInput
8036
- style={styles.inputLast}
8037
- placeholder="Password"
8038
- value={password}
8039
- onChangeText={setPassword}
8040
- secureTextEntry
8041
- />
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>
8042
9044
 
8043
- <TouchableOpacity
8044
- onPress={handleSignUp}
8045
- disabled={isLoading}
8046
- style={styles.button}
8047
- >
8048
- {isLoading ? (
8049
- <ActivityIndicator size="small" color="#fff" />
8050
- ) : (
8051
- <Text style={styles.buttonText}>Sign Up</Text>
8052
- )}
8053
- </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>
8054
9060
  </View>
8055
9061
  );
8056
9062
  }
@@ -8237,86 +9243,171 @@ import { queryClient } from "@/utils/trpc";
8237
9243
  {{#if (eq api "orpc")}}
8238
9244
  import { queryClient } from "@/utils/orpc";
8239
9245
  {{/if}}
8240
- import { useState } from "react";
8241
- import { Text, View } from "react-native";
8242
- 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";
8243
9251
 
8244
- function SignIn() {
8245
- const [email, setEmail] = useState("");
8246
- const [password, setPassword] = useState("");
8247
- const [isLoading, setIsLoading] = useState(false);
8248
- 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
+ });
8249
9263
 
8250
- async function handleLogin() {
8251
- setIsLoading(true);
8252
- setError(null);
9264
+ function getErrorMessage(error: unknown): string | null {
9265
+ if (!error) return null;
8253
9266
 
8254
- await authClient.signIn.email(
8255
- {
8256
- email,
8257
- password,
8258
- },
8259
- {
8260
- onError(error) {
8261
- setError(error.error?.message || "Failed to sign in");
8262
- setIsLoading(false);
8263
- },
8264
- onSuccess() {
8265
- setEmail("");
8266
- setPassword("");
8267
- {{#if (eq api "orpc")}}
8268
- queryClient.refetchQueries();
8269
- {{/if}}
8270
- {{#if (eq api "trpc")}}
8271
- queryClient.refetchQueries();
8272
- {{/if}}
8273
- },
8274
- onFinished() {
8275
- setIsLoading(false);
8276
- },
9267
+ if (typeof error === "string") {
9268
+ return error;
8277
9269
  }
8278
- );
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
+ }
8279
9286
  }
8280
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
+
8281
9334
  return (
8282
- <Surface variant="secondary" className="p-4 rounded-lg">
8283
- <Text className="text-foreground font-medium mb-4">Sign In</Text>
8284
-
8285
- <ErrorView isInvalid={!!error} className="mb-3">
8286
- {error}
8287
- </ErrorView>
8288
-
8289
- <View className="gap-3">
8290
- <TextField>
8291
- <TextField.Label>Email</TextField.Label>
8292
- <TextField.Input
8293
- value={email}
8294
- onChangeText={setEmail}
8295
- placeholder="email@example.com"
8296
- keyboardType="email-address"
8297
- autoCapitalize="none"
8298
- />
8299
- </TextField>
8300
-
8301
- <TextField>
8302
- <TextField.Label>Password</TextField.Label>
8303
- <TextField.Input
8304
- value={password}
8305
- onChangeText={setPassword}
8306
- placeholder="••••••••"
8307
- secureTextEntry
8308
- />
8309
- </TextField>
9335
+ <Surface variant="secondary" className="p-4 rounded-lg">
9336
+ <Text className="text-foreground font-medium mb-4">Sign In</Text>
8310
9337
 
8311
- <Button onPress={handleLogin} isDisabled={isLoading} className="mt-1">
8312
- {isLoading ? <Spinner size="sm" color="default" /> : <Button.Label>Sign In</Button.Label>}
8313
- </Button>
8314
- </View>
8315
- </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>
8316
9406
  );
8317
- }
9407
+ }
8318
9408
 
8319
- export { SignIn };`],
9409
+ export { SignIn };
9410
+ `],
8320
9411
  ["auth/better-auth/native/uniwind/components/sign-up.tsx.hbs", `import { authClient } from "@/lib/auth-client";
8321
9412
  {{#if (eq api "trpc")}}
8322
9413
  import { queryClient } from "@/utils/trpc";
@@ -8324,127 +9415,203 @@ import { queryClient } from "@/utils/trpc";
8324
9415
  {{#if (eq api "orpc")}}
8325
9416
  import { queryClient } from "@/utils/orpc";
8326
9417
  {{/if}}
8327
- import { useState } from "react";
8328
- import { Text, View } from "react-native";
8329
- import { Button, ErrorView, Spinner, Surface, TextField } from "heroui-native";
8330
-
8331
- function signUpHandler({
8332
- name,
8333
- email,
8334
- password,
8335
- setError,
8336
- setIsLoading,
8337
- setName,
8338
- setEmail,
8339
- setPassword,
8340
- }: {
8341
- name: string;
8342
- email: string;
8343
- password: string;
8344
- setError: (error: string | null) => void;
8345
- setIsLoading: (loading: boolean) => void;
8346
- setName: (name: string) => void;
8347
- setEmail: (email: string) => void;
8348
- setPassword: (password: string) => void;
8349
- }) {
8350
- setIsLoading(true);
8351
- setError(null);
8352
-
8353
- authClient.signUp.email(
8354
- {
8355
- name,
8356
- email,
8357
- password,
8358
- },
8359
- {
8360
- onError(error) {
8361
- setError(error.error?.message || "Failed to sign up");
8362
- setIsLoading(false);
8363
- },
8364
- onSuccess() {
8365
- setName("");
8366
- setEmail("");
8367
- setPassword("");
8368
- {{#if (eq api "orpc")}}
8369
- queryClient.refetchQueries();
8370
- {{/if}}
8371
- {{#if (eq api "trpc")}}
8372
- queryClient.refetchQueries();
8373
- {{/if}}
8374
- },
8375
- onFinished() {
8376
- setIsLoading(false);
8377
- },
8378
- }
8379
- );
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;
8380
9466
  }
8381
9467
 
8382
9468
  export function SignUp() {
8383
- const [name, setName] = useState("");
8384
- const [email, setEmail] = useState("");
8385
- const [password, setPassword] = useState("");
8386
- const [isLoading, setIsLoading] = useState(false);
8387
- const [error, setError] = useState<string | null>(null);
8388
-
8389
- function handlePress() {
8390
- signUpHandler({
8391
- name,
8392
- email,
8393
- password,
8394
- setError,
8395
- setIsLoading,
8396
- setName,
8397
- setEmail,
8398
- 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
+ },
8399
9512
  });
8400
- }
8401
9513
 
8402
9514
  return (
8403
- <Surface variant="secondary" className="p-4 rounded-lg">
8404
- <Text className="text-foreground font-medium mb-4">Create Account</Text>
8405
-
8406
- <ErrorView isInvalid={!!error} className="mb-3">
8407
- {error}
8408
- </ErrorView>
8409
-
8410
- <View className="gap-3">
8411
- <TextField>
8412
- <TextField.Label>Name</TextField.Label>
8413
- <TextField.Input value={name} onChangeText={setName} placeholder="John Doe" />
8414
- </TextField>
8415
-
8416
- <TextField>
8417
- <TextField.Label>Email</TextField.Label>
8418
- <TextField.Input
8419
- value={email}
8420
- onChangeText={setEmail}
8421
- placeholder="email@example.com"
8422
- keyboardType="email-address"
8423
- autoCapitalize="none"
8424
- />
8425
- </TextField>
8426
-
8427
- <TextField>
8428
- <TextField.Label>Password</TextField.Label>
8429
- <TextField.Input
8430
- value={password}
8431
- onChangeText={setPassword}
8432
- placeholder="••••••••"
8433
- secureTextEntry
8434
- />
8435
- </TextField>
9515
+ <Surface variant="secondary" className="p-4 rounded-lg">
9516
+ <Text className="text-foreground font-medium mb-4">Create Account</Text>
8436
9517
 
8437
- <Button onPress={handlePress} isDisabled={isLoading} className="mt-1">
8438
- {isLoading ? (
8439
- <Spinner size="sm" color="default" />
8440
- ) : (
8441
- <Button.Label>Create Account</Button.Label>
8442
- )}
8443
- </Button>
8444
- </View>
8445
- </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>
8446
9612
  );
8447
- }`],
9613
+ }
9614
+ `],
8448
9615
  ["auth/better-auth/server/base/_gitignore", `# dependencies (bun install)
8449
9616
  node_modules
8450
9617
 
@@ -16373,7 +17540,7 @@ import {
16373
17540
  } from "@convex-dev/agent/react";
16374
17541
  import { api } from "@{{projectName}}/backend/convex/_generated/api";
16375
17542
  import { useMutation } from "convex/react";
16376
- import { Button, Divider, Spinner, Surface, TextField, useThemeColor } from "heroui-native";
17543
+ import { Button, Separator, Spinner, Surface, Input, TextField, useThemeColor } from "heroui-native";
16377
17544
  import { useRef, useEffect, useState } from "react";
16378
17545
  import {
16379
17546
  View,
@@ -16446,7 +17613,7 @@ export default function AIScreen() {
16446
17613
  };
16447
17614
 
16448
17615
  return (
16449
- <Container>
17616
+ <Container isScrollable={false}>
16450
17617
  <KeyboardAvoidingView
16451
17618
  className="flex-1"
16452
17619
  behavior={Platform.OS === "ios" ? "padding" : "height"}
@@ -16461,19 +17628,21 @@ export default function AIScreen() {
16461
17628
  ref={scrollViewRef}
16462
17629
  className="flex-1 mb-4"
16463
17630
  showsVerticalScrollIndicator={false}
17631
+ contentContainerStyle=\\{{ flexGrow: 1, paddingBottom: 8 }}
17632
+ keyboardShouldPersistTaps="handled"
16464
17633
  >
16465
17634
  {!messages || messages.length === 0 ? (
16466
- <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">
16467
17636
  <Ionicons name="chatbubble-ellipses-outline" size={32} color={mutedColor} />
16468
17637
  <Text className="text-muted text-sm mt-3">Ask me anything to get started</Text>
16469
- </View>
17638
+ </Surface>
16470
17639
  ) : (
16471
- <View className="gap-2">
17640
+ <View className="gap-3">
16472
17641
  {messages.map((message: UIMessage) => (
16473
17642
  <Surface
16474
17643
  key={message.key}
16475
17644
  variant={message.role === "user" ? "tertiary" : "secondary"}
16476
- 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"}\`}
16477
17646
  >
16478
17647
  <Text className="text-xs font-medium mb-1 text-muted">
16479
17648
  {message.role === "user" ? "You" : "AI"}
@@ -16497,21 +17666,21 @@ export default function AIScreen() {
16497
17666
  )}
16498
17667
  </ScrollView>
16499
17668
 
16500
- <Divider className="mb-3" />
17669
+ <Separator className="mb-3" />
16501
17670
 
16502
17671
  <View className="flex-row items-center gap-2">
16503
17672
  <View className="flex-1">
16504
- <TextField>
16505
- <TextField.Input
16506
- value={input}
16507
- onChangeText={setInput}
16508
- placeholder="Type a message..."
16509
- onSubmitEditing={onSubmit}
16510
- editable={!isLoading}
16511
- autoFocus
16512
- />
16513
- </TextField>
16514
- </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>
16515
17684
  <Button
16516
17685
  isIconOnly
16517
17686
  variant={input.trim() && !isLoading ? "primary" : "secondary"}
@@ -16545,7 +17714,7 @@ import { DefaultChatTransport } from "ai";
16545
17714
  import { fetch as expoFetch } from "expo/fetch";
16546
17715
  import { Ionicons } from "@expo/vector-icons";
16547
17716
  import { Container } from "@/components/container";
16548
- import { Button, Divider, ErrorView, Spinner, Surface, TextField, useThemeColor } from "heroui-native";
17717
+ import { Button, Separator, FieldError, Spinner, Surface, Input, TextField, useThemeColor } from "heroui-native";
16549
17718
  import { env } from "@{{projectName}}/env/native";
16550
17719
 
16551
17720
  const generateAPIUrl = (relativePath: string) => {
@@ -16561,7 +17730,7 @@ const generateAPIUrl = (relativePath: string) => {
16561
17730
 
16562
17731
  export default function AIScreen() {
16563
17732
  const [input, setInput] = useState("");
16564
- const { messages, error, sendMessage } = useChat({
17733
+ const { messages, error, sendMessage, status } = useChat({
16565
17734
  transport: new DefaultChatTransport({
16566
17735
  fetch: expoFetch as unknown as typeof globalThis.fetch,
16567
17736
  api: generateAPIUrl("/ai"),
@@ -16571,6 +17740,7 @@ export default function AIScreen() {
16571
17740
  const scrollViewRef = useRef<ScrollView>(null);
16572
17741
  const foregroundColor = useThemeColor("foreground");
16573
17742
  const mutedColor = useThemeColor("muted");
17743
+ const isBusy = status === "submitted" || status === "streaming";
16574
17744
 
16575
17745
  useEffect(() => {
16576
17746
  scrollViewRef.current?.scrollToEnd({ animated: true });
@@ -16578,7 +17748,7 @@ export default function AIScreen() {
16578
17748
 
16579
17749
  const onSubmit = () => {
16580
17750
  const value = input.trim();
16581
- if (value) {
17751
+ if (value && !isBusy) {
16582
17752
  sendMessage({ text: value });
16583
17753
  setInput("");
16584
17754
  }
@@ -16586,17 +17756,17 @@ export default function AIScreen() {
16586
17756
 
16587
17757
  if (error) {
16588
17758
  return (
16589
- <Container>
17759
+ <Container isScrollable={false}>
16590
17760
  <View className="flex-1 justify-center items-center px-4">
16591
17761
  <Surface variant="secondary" className="p-4 rounded-lg">
16592
- <ErrorView isInvalid>
17762
+ <FieldError isInvalid>
16593
17763
  <Text className="text-danger text-center font-medium mb-1">
16594
17764
  {error.message}
16595
17765
  </Text>
16596
17766
  <Text className="text-muted text-center text-xs">
16597
17767
  Please check your connection and try again.
16598
17768
  </Text>
16599
- </ErrorView>
17769
+ </FieldError>
16600
17770
  </Surface>
16601
17771
  </View>
16602
17772
  </Container>
@@ -16604,7 +17774,7 @@ export default function AIScreen() {
16604
17774
  }
16605
17775
 
16606
17776
  return (
16607
- <Container>
17777
+ <Container isScrollable={false}>
16608
17778
  <KeyboardAvoidingView
16609
17779
  className="flex-1"
16610
17780
  behavior={Platform.OS === "ios" ? "padding" : "height"}
@@ -16619,19 +17789,21 @@ export default function AIScreen() {
16619
17789
  ref={scrollViewRef}
16620
17790
  className="flex-1 mb-4"
16621
17791
  showsVerticalScrollIndicator={false}
17792
+ contentContainerStyle=\\{{ flexGrow: 1, paddingBottom: 8 }}
17793
+ keyboardShouldPersistTaps="handled"
16622
17794
  >
16623
17795
  {messages.length === 0 ? (
16624
- <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">
16625
17797
  <Ionicons name="chatbubble-ellipses-outline" size={32} color={mutedColor} />
16626
17798
  <Text className="text-muted text-sm mt-3">Ask me anything to get started</Text>
16627
- </View>
17799
+ </Surface>
16628
17800
  ) : (
16629
- <View className="gap-2">
17801
+ <View className="gap-3">
16630
17802
  {messages.map((message) => (
16631
17803
  <Surface
16632
17804
  key={message.id}
16633
17805
  variant={message.role === "user" ? "tertiary" : "secondary"}
16634
- 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"}\`}
16635
17807
  >
16636
17808
  <Text className="text-xs font-medium mb-1 text-muted">
16637
17809
  {message.role === "user" ? "You" : "AI"}
@@ -16657,35 +17829,45 @@ export default function AIScreen() {
16657
17829
  </View>
16658
17830
  </Surface>
16659
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
+ )}
16660
17841
  </View>
16661
17842
  )}
16662
17843
  </ScrollView>
16663
17844
 
16664
- <Divider className="mb-3" />
17845
+ <Separator className="mb-3" />
16665
17846
 
16666
17847
  <View className="flex-row items-center gap-2">
16667
17848
  <View className="flex-1">
16668
17849
  <TextField>
16669
- <TextField.Input
17850
+ <Input
16670
17851
  value={input}
16671
17852
  onChangeText={setInput}
16672
17853
  placeholder="Type a message..."
16673
17854
  onSubmitEditing={onSubmit}
16674
- autoFocus
17855
+ returnKeyType="send"
17856
+ editable={!isBusy}
16675
17857
  />
16676
17858
  </TextField>
16677
17859
  </View>
16678
17860
  <Button
16679
17861
  isIconOnly
16680
- variant={input.trim() ? "primary" : "secondary"}
17862
+ variant={input.trim() && !isBusy ? "primary" : "secondary"}
16681
17863
  onPress={onSubmit}
16682
- isDisabled={!input.trim()}
17864
+ isDisabled={!input.trim() || isBusy}
16683
17865
  size="sm"
16684
17866
  >
16685
17867
  <Ionicons
16686
17868
  name="arrow-up"
16687
17869
  size={18}
16688
- color={input.trim() ? foregroundColor : mutedColor}
17870
+ color={input.trim() && !isBusy ? foregroundColor : mutedColor}
16689
17871
  />
16690
17872
  </Button>
16691
17873
  </View>
@@ -18795,7 +19977,7 @@ import { Container } from "@/components/container";
18795
19977
  import { trpc } from "@/utils/trpc";
18796
19978
  {{/if}}
18797
19979
  {{/unless}}
18798
- import { Button, Checkbox, Chip, Spinner, Surface, TextField, useThemeColor } from "heroui-native";
19980
+ import { Button, Checkbox, Chip, Spinner, Surface, Input, TextField, useThemeColor } from "heroui-native";
18799
19981
 
18800
19982
  export default function TodosScreen() {
18801
19983
  const [newTodoText, setNewTodoText] = useState("");
@@ -18922,7 +20104,7 @@ export default function TodosScreen() {
18922
20104
  <View className="flex-row items-center gap-2">
18923
20105
  <View className="flex-1">
18924
20106
  <TextField>
18925
- <TextField.Input
20107
+ <Input
18926
20108
  value={newTodoText}
18927
20109
  onChangeText={setNewTodoText}
18928
20110
  placeholder="Add a new task..."
@@ -22498,7 +23680,6 @@ module.exports = config;
22498
23680
  "@react-navigation/bottom-tabs": "^7.2.0",
22499
23681
  "@react-navigation/drawer": "^7.1.1",
22500
23682
  "@react-navigation/native": "^7.0.14",
22501
- "@tanstack/react-form": "^1.0.5",
22502
23683
  "@tanstack/react-query": "^5.85.5",
22503
23684
  {{#if (includes examples "ai")}}
22504
23685
  "@stardazed/streams-text-encoding": "^1.0.2",
@@ -22532,7 +23713,6 @@ module.exports = config;
22532
23713
  },
22533
23714
  "private": true
22534
23715
  }
22535
-
22536
23716
  `],
22537
23717
  ["frontend/native/bare/tsconfig.json.hbs", `{
22538
23718
  "extends": "expo/tsconfig.base",
@@ -23560,7 +24740,6 @@ module.exports = config;
23560
24740
  "@stardazed/streams-text-encoding": "^1.0.2",
23561
24741
  "@ungap/structured-clone": "^1.3.0",
23562
24742
  {{/if}}
23563
- "@tanstack/react-form": "^1.0.5",
23564
24743
  "babel-preset-expo": "~54.0.10",
23565
24744
  "expo": "~54.0.33",
23566
24745
  "expo-constants": "~18.0.8",
@@ -24107,7 +25286,7 @@ import { api } from "@{{projectName}}/backend/convex/_generated/api";
24107
25286
  {{#unless (or (eq backend "none") (and (eq backend "convex") (eq auth "better-auth")))}}
24108
25287
  import { Ionicons } from "@expo/vector-icons";
24109
25288
  {{/unless}}
24110
- import { Button, Chip, Divider, Spinner, Surface, useThemeColor } from "heroui-native";
25289
+ import { Button, Chip, Separator, Spinner, Surface, useThemeColor } from "heroui-native";
24111
25290
 
24112
25291
  export default function Home() {
24113
25292
  {{#if (eq api "orpc")}}
@@ -24143,8 +25322,8 @@ const isLoading = healthCheck?.isLoading;
24143
25322
  {{/unless}}
24144
25323
 
24145
25324
  return (
24146
- <Container className="p-4">
24147
- <View className="py-6 mb-4">
25325
+ <Container className="px-4 pb-4">
25326
+ <View className="py-6 mb-5">
24148
25327
  <Text className="text-3xl font-semibold text-foreground tracking-tight">
24149
25328
  Better T Stack
24150
25329
  </Text>
@@ -24152,7 +25331,7 @@ return (
24152
25331
  </View>
24153
25332
 
24154
25333
  {{#unless (or (eq backend "none") (and (eq backend "convex") (eq auth "better-auth")))}}
24155
- <Surface variant="secondary" className="p-4 rounded-lg">
25334
+ <Surface variant="secondary" className="p-4 rounded-xl">
24156
25335
  <View className="flex-row items-center justify-between mb-3">
24157
25336
  <Text className="text-foreground font-medium">System Status</Text>
24158
25337
  <Chip variant="secondary" color={isConnected ? "success" : "danger" } size="sm">
@@ -24162,9 +25341,9 @@ return (
24162
25341
  </Chip>
24163
25342
  </View>
24164
25343
 
24165
- <Divider className="mb-3" />
25344
+ <Separator className="mb-3" />
24166
25345
 
24167
- <Surface variant="tertiary" className="p-3 rounded-md">
25346
+ <Surface variant="tertiary" className="p-3 rounded-lg">
24168
25347
  <View className="flex-row items-center">
24169
25348
  <View className={\`w-2 h-2 rounded-full mr-3 \${ isConnected ? "bg-success" : "bg-muted" }\`} />
24170
25349
  <View className="flex-1">
@@ -24199,7 +25378,7 @@ return (
24199
25378
 
24200
25379
  {{#if (and (eq backend "convex") (eq auth "clerk"))}}
24201
25380
  <Authenticated>
24202
- <Surface variant="secondary" className="mt-4 p-4 rounded-lg">
25381
+ <Surface variant="secondary" className="mt-5 p-4 rounded-xl">
24203
25382
  <View className="flex-row items-center justify-between">
24204
25383
  <View className="flex-1">
24205
25384
  <Text className="text-foreground font-medium">{user?.emailAddresses[0].emailAddress}</Text>
@@ -24228,14 +25407,14 @@ return (
24228
25407
 
24229
25408
  {{#if (and (eq backend "convex") (eq auth "better-auth"))}}
24230
25409
  {user ? (
24231
- <Surface variant="secondary" className="mb-4 p-4 rounded-lg">
25410
+ <Surface variant="secondary" className="mb-4 p-4 rounded-xl">
24232
25411
  <View className="flex-row items-center justify-between">
24233
25412
  <View className="flex-1">
24234
25413
  <Text className="text-foreground font-medium">{user.name}</Text>
24235
25414
  <Text className="text-muted text-xs mt-0.5">{user.email}</Text>
24236
25415
  </View>
24237
25416
  <Button
24238
- variant="destructive"
25417
+ variant="danger"
24239
25418
  size="sm"
24240
25419
  onPress={() => {
24241
25420
  authClient.signOut();
@@ -24246,7 +25425,7 @@ return (
24246
25425
  </View>
24247
25426
  </Surface>
24248
25427
  ) : null}
24249
- <Surface variant="secondary" className="p-4 rounded-lg">
25428
+ <Surface variant="secondary" className="p-4 rounded-xl">
24250
25429
  <Text className="text-foreground font-medium mb-2">API Status</Text>
24251
25430
  <View className="flex-row items-center gap-2">
24252
25431
  <View className={\`w-2 h-2 rounded-full \${healthCheck==="OK" ? "bg-success" : "bg-danger" }\`} />
@@ -24260,7 +25439,7 @@ return (
24260
25439
  </View>
24261
25440
  </Surface>
24262
25441
  {!user && (
24263
- <View className="mt-4 gap-4">
25442
+ <View className="mt-5 gap-4">
24264
25443
  <SignIn />
24265
25444
  <SignUp />
24266
25445
  </View>
@@ -24268,7 +25447,8 @@ return (
24268
25447
  {{/if}}
24269
25448
  </Container>
24270
25449
  );
24271
- }`],
25450
+ }
25451
+ `],
24272
25452
  ["frontend/native/uniwind/app/+not-found.tsx.hbs", `import { Link, Stack } from "expo-router";
24273
25453
  import { Button, Surface } from "heroui-native";
24274
25454
  import { Text, View } from "react-native";
@@ -24337,36 +25517,49 @@ export default Modal;
24337
25517
  `],
24338
25518
  ["frontend/native/uniwind/components/container.tsx.hbs", `import { cn } from "heroui-native";
24339
25519
  import { type PropsWithChildren } from "react";
24340
- import { ScrollView, View, type ViewProps } from "react-native";
25520
+ import { ScrollView, View, type ScrollViewProps, type ViewProps } from "react-native";
24341
25521
  import Animated, { type AnimatedProps } from "react-native-reanimated";
24342
25522
  import { useSafeAreaInsets } from "react-native-safe-area-context";
24343
25523
 
24344
25524
  const AnimatedView = Animated.createAnimatedComponent(View);
24345
25525
 
24346
25526
  type Props = AnimatedProps<ViewProps> & {
24347
- className?: string;
25527
+ className?: string;
25528
+ isScrollable?: boolean;
25529
+ scrollViewProps?: Omit<ScrollViewProps, "contentContainerStyle">;
24348
25530
  };
24349
25531
 
24350
25532
  export function Container({
24351
- children,
24352
- className,
24353
- ...props
25533
+ children,
25534
+ className,
25535
+ isScrollable = true,
25536
+ scrollViewProps,
25537
+ ...props
24354
25538
  }: PropsWithChildren<Props>) {
24355
- const insets = useSafeAreaInsets();
25539
+ const insets = useSafeAreaInsets();
24356
25540
 
24357
- return (
24358
- <AnimatedView
24359
- className={cn("flex-1 bg-background", className)}
24360
- style=\\{{
24361
- paddingBottom: insets.bottom,
24362
- }}
24363
- {...props}
24364
- >
24365
- <ScrollView contentContainerStyle=\\{{ flexGrow: 1 }}>
24366
- {children}
24367
- </ScrollView>
24368
- </AnimatedView>
24369
- );
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
+ );
24370
25563
  }
24371
25564
  `],
24372
25565
  ["frontend/native/uniwind/components/theme-toggle.tsx.hbs", `import { Ionicons } from '@expo/vector-icons';
@@ -24476,17 +25669,17 @@ export function useAppTheme() {
24476
25669
  `],
24477
25670
  ["frontend/native/uniwind/metro.config.js.hbs", `const { getDefaultConfig } = require("expo/metro-config");
24478
25671
  const { withUniwindConfig } = require("uniwind/metro");
25672
+ const { wrapWithReanimatedMetroConfig } = require("react-native-reanimated/metro-config");
24479
25673
 
24480
25674
  /** @type {import('expo/metro-config').MetroConfig} */
24481
25675
  const config = getDefaultConfig(__dirname);
24482
25676
 
24483
- const uniwindConfig = withUniwindConfig(config, {
25677
+ const uniwindConfig = withUniwindConfig(wrapWithReanimatedMetroConfig(config), {
24484
25678
  cssEntryFile: "./global.css",
24485
25679
  dtsFile: "./uniwind-types.d.ts",
24486
25680
  });
24487
25681
 
24488
25682
  module.exports = uniwindConfig;
24489
-
24490
25683
  `],
24491
25684
  ["frontend/native/uniwind/package.json.hbs", `{
24492
25685
  "name": "native",
@@ -24520,7 +25713,7 @@ module.exports = uniwindConfig;
24520
25713
  "expo-router": "~6.0.14",
24521
25714
  "expo-secure-store": "~15.0.7",
24522
25715
  "expo-status-bar": "~3.0.8",
24523
- "heroui-native": "^1.0.0-beta.9",
25716
+ "heroui-native": "^1.0.0-rc.1",
24524
25717
  "react": "19.1.0",
24525
25718
  "react-dom": "19.1.0",
24526
25719
  "react-native": "0.81.5",
@@ -24535,13 +25728,14 @@ module.exports = uniwindConfig;
24535
25728
  "tailwind-merge": "^3.4.0",
24536
25729
  "tailwind-variants": "^3.2.2",
24537
25730
  "tailwindcss": "^4.1.18",
24538
- "uniwind": "^1.2.2"
25731
+ "uniwind": "^1.3.0"
24539
25732
  },
24540
25733
  "devDependencies": {
24541
25734
  "@types/node": "^24.10.0",
24542
25735
  "@types/react": "~19.1.0"
24543
25736
  }
24544
- }`],
25737
+ }
25738
+ `],
24545
25739
  ["frontend/native/uniwind/tsconfig.json.hbs", `{
24546
25740
  "extends": "expo/tsconfig.base",
24547
25741
  "compilerOptions": {
@@ -24894,7 +26088,6 @@ initOpenNextCloudflareForDev();
24894
26088
  "dependencies": {
24895
26089
  "@base-ui/react": "^1.0.0",
24896
26090
  "shadcn": "^3.6.2",
24897
- "@tanstack/react-form": "^1.27.3",
24898
26091
  "class-variance-authority": "^0.7.1",
24899
26092
  "clsx": "^2.1.1",
24900
26093
  "lucide-react": "^0.546.0",
@@ -25279,7 +26472,6 @@ export function ThemeProvider({
25279
26472
  "@react-router/fs-routes": "^7.10.1",
25280
26473
  "@react-router/node": "^7.10.1",
25281
26474
  "@react-router/serve": "^7.10.1",
25282
- "@tanstack/react-form": "^1.27.3",
25283
26475
  "class-variance-authority": "^0.7.1",
25284
26476
  "clsx": "^2.1.1",
25285
26477
  "isbot": "^5.1.28",
@@ -25708,7 +26900,6 @@ export default defineConfig({
25708
26900
  "@hookform/resolvers": "^5.1.1",
25709
26901
  "@base-ui/react": "^1.0.0",
25710
26902
  "shadcn": "^3.6.2",
25711
- "@tanstack/react-form": "^1.12.3",
25712
26903
  "@tailwindcss/vite": "^4.0.15",
25713
26904
  "@tanstack/react-router": "^1.141.1",
25714
26905
  "class-variance-authority": "^0.7.1",
@@ -26109,7 +27300,6 @@ export default defineConfig({
26109
27300
  "dependencies": {
26110
27301
  "@base-ui/react": "^1.0.0",
26111
27302
  "shadcn": "^3.6.2",
26112
- "@tanstack/react-form": "^1.23.5",
26113
27303
  "@tailwindcss/vite": "^4.1.8",
26114
27304
  "@tanstack/react-query": "^5.80.6",
26115
27305
  "@tanstack/react-router": "^1.141.1",
@@ -27542,7 +28732,6 @@ dist-ssr
27542
28732
  "dependencies": {
27543
28733
  "@tailwindcss/vite": "^4.1.13",
27544
28734
  "@tanstack/router-plugin": "^1.131.44",
27545
- "@tanstack/solid-form": "^1.20.0",
27546
28735
  "@tanstack/solid-router": "^1.131.44",
27547
28736
  "lucide-solid": "^0.544.0",
27548
28737
  "solid-js": "^1.9.9",
@@ -27864,9 +29053,7 @@ vite.config.ts.timestamp-*
27864
29053
  "tailwindcss": "^4.1.12",
27865
29054
  "vite": "^7.1.2"
27866
29055
  },
27867
- "dependencies": {
27868
- "@tanstack/svelte-form": "^1.19.2"
27869
- }
29056
+ "dependencies": {}
27870
29057
  }
27871
29058
  `],
27872
29059
  ["frontend/svelte/src/app.css", `@import "tailwindcss";