create-100x-mobile 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,7 @@ const projectName_1 = require("../lib/projectName");
12
12
  const run_1 = require("../lib/run");
13
13
  const dotenv_1 = require("../lib/dotenv");
14
14
  const fs_2 = require("../lib/fs");
15
+ const clerk_1 = require("../lib/clerk");
15
16
  // Config templates
16
17
  const packageJson_1 = require("../templates/config/packageJson");
17
18
  const appJson_1 = require("../templates/config/appJson");
@@ -29,7 +30,6 @@ const notFound_1 = require("../templates/app/notFound");
29
30
  const authProvider_1 = require("../templates/app/authProvider");
30
31
  const authLayout_1 = require("../templates/app/authLayout");
31
32
  const signIn_1 = require("../templates/app/signIn");
32
- const signUp_1 = require("../templates/app/signUp");
33
33
  const tabsLayout_1 = require("../templates/app/tabsLayout");
34
34
  const todosScreen_1 = require("../templates/app/todosScreen");
35
35
  const settingsScreen_1 = require("../templates/app/settingsScreen");
@@ -121,7 +121,6 @@ async function cmdNew(args) {
121
121
  ["app/providers/AuthProvider.tsx", (0, authProvider_1.authProviderTemplate)()],
122
122
  ["app/(auth)/_layout.tsx", (0, authLayout_1.authLayoutTemplate)()],
123
123
  ["app/(auth)/sign-in.tsx", (0, signIn_1.signInTemplate)()],
124
- ["app/(auth)/sign-up.tsx", (0, signUp_1.signUpTemplate)()],
125
124
  ["app/(tabs)/_layout.tsx", (0, tabsLayout_1.tabsLayoutTemplate)()],
126
125
  ["app/(tabs)/index.tsx", (0, todosScreen_1.todosScreenTemplate)()],
127
126
  ["app/(tabs)/settings.tsx", (0, settingsScreen_1.settingsScreenTemplate)()],
@@ -173,19 +172,38 @@ async function cmdNew(args) {
173
172
  });
174
173
  const clerkKeyValue = (0, prompts_1.isCancel)(clerkKey) || !clerkKey ? "" : clerkKey.trim();
175
174
  let clerkDomain = "";
175
+ let clerkSecretKey = "";
176
176
  if (clerkKeyValue) {
177
- const domainInput = await (0, prompts_1.text)({
178
- message: "Clerk JWT Issuer Domain",
179
- placeholder: "https://your-app.clerk.accounts.dev",
177
+ // Try to extract domain from publishable key
178
+ const derivedDomain = (0, clerk_1.extractDomainFromPublishableKey)(clerkKeyValue);
179
+ if (derivedDomain) {
180
+ clerkDomain = derivedDomain;
181
+ prompts_1.log.info(` Detected issuer domain: ${picocolors_1.default.cyan(clerkDomain)}`);
182
+ }
183
+ else {
184
+ const domainInput = await (0, prompts_1.text)({
185
+ message: "Clerk JWT Issuer Domain",
186
+ placeholder: "https://your-app.clerk.accounts.dev",
187
+ defaultValue: "",
188
+ });
189
+ clerkDomain =
190
+ (0, prompts_1.isCancel)(domainInput) || !domainInput ? "" : domainInput.trim();
191
+ }
192
+ // Ask for secret key to auto-create JWT template
193
+ prompts_1.log.info("");
194
+ prompts_1.log.info(picocolors_1.default.dim('Provide your Clerk Secret Key to auto-create the "convex" JWT template.'));
195
+ prompts_1.log.info(picocolors_1.default.dim("Press Enter to skip (you can create it manually later)."));
196
+ const secretInput = await (0, prompts_1.text)({
197
+ message: "Clerk Secret Key",
198
+ placeholder: "sk_test_...",
180
199
  defaultValue: "",
181
200
  });
182
- clerkDomain =
183
- (0, prompts_1.isCancel)(domainInput) || !domainInput ? "" : domainInput.trim();
201
+ clerkSecretKey =
202
+ (0, prompts_1.isCancel)(secretInput) || !secretInput ? "" : secretInput.trim();
184
203
  }
185
204
  // Step 7: Write env vars
186
205
  const envLocalPath = (0, node_path_1.join)(projectDir, ".env.local");
187
206
  let envContents = "";
188
- // Read existing .env.local (Convex may have written EXPO_PUBLIC_CONVEX_URL)
189
207
  try {
190
208
  envContents = await (0, fs_2.readTextFile)(envLocalPath);
191
209
  }
@@ -212,6 +230,22 @@ async function cmdNew(args) {
212
230
  prompts_1.log.warn(`Run manually: bunx convex env set CLERK_JWT_ISSUER_DOMAIN ${clerkDomain}`);
213
231
  }
214
232
  }
233
+ // Step 8b: Auto-create JWT template if secret key provided
234
+ let jwtCreated = false;
235
+ if (clerkSecretKey) {
236
+ const jwtSpinner = (0, prompts_1.spinner)();
237
+ jwtSpinner.start('Creating "convex" JWT template in Clerk');
238
+ const result = await (0, clerk_1.createConvexJwtTemplate)(clerkSecretKey);
239
+ if (result.success) {
240
+ jwtSpinner.stop('JWT template "convex" created in Clerk.');
241
+ jwtCreated = true;
242
+ }
243
+ else {
244
+ jwtSpinner.stop("Could not create JWT template.");
245
+ prompts_1.log.warn(`Clerk API error: ${result.error}`);
246
+ prompts_1.log.warn('Create a JWT template named "convex" manually in Clerk dashboard.');
247
+ }
248
+ }
215
249
  // Step 9: Push Convex functions (if Clerk was configured)
216
250
  if (clerkKeyValue && clerkDomain) {
217
251
  prompts_1.log.step("Deploying Convex functions");
@@ -225,29 +259,44 @@ async function cmdNew(args) {
225
259
  }
226
260
  // Step 10: Success message
227
261
  prompts_1.log.info("");
228
- if (clerkKeyValue && clerkDomain) {
262
+ if (clerkKeyValue && clerkDomain && jwtCreated) {
263
+ // Fully automated — everything is set up
229
264
  prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green("Your app is ready!")));
230
265
  prompts_1.log.info("");
231
266
  prompts_1.log.info(" Next steps:");
232
267
  prompts_1.log.info(` ${picocolors_1.default.cyan("cd")} ${projectName}`);
233
268
  prompts_1.log.info(` ${picocolors_1.default.cyan("bunx expo start")}`);
234
269
  prompts_1.log.info("");
235
- prompts_1.log.info(picocolors_1.default.dim('Important: In your Clerk dashboard, create a JWT template named "convex"'));
236
- prompts_1.log.info(picocolors_1.default.dim("with issuer set to your Convex deployment URL."));
270
+ prompts_1.log.info(picocolors_1.default.dim("Remember to enable Google and Apple OAuth providers in your Clerk dashboard."));
271
+ }
272
+ else if (clerkKeyValue && clerkDomain) {
273
+ // Keys set but JWT template not auto-created
274
+ prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green("Almost ready!")));
275
+ prompts_1.log.info("");
276
+ prompts_1.log.info(" One more step:");
277
+ prompts_1.log.info(` 1. Create a JWT template named ${picocolors_1.default.bold('"convex"')} in Clerk dashboard`);
278
+ prompts_1.log.info(picocolors_1.default.dim(" Dashboard → JWT Templates → New template → Name: convex"));
279
+ prompts_1.log.info(` 2. Enable Google and Apple OAuth in Clerk dashboard`);
280
+ prompts_1.log.info("");
281
+ prompts_1.log.info(" Then start your app:");
282
+ prompts_1.log.info(` ${picocolors_1.default.cyan("cd")} ${projectName}`);
283
+ prompts_1.log.info(` ${picocolors_1.default.cyan("bunx expo start")}`);
237
284
  }
238
285
  else {
286
+ // No Clerk keys — manual setup needed
239
287
  prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green("Project scaffolded!")));
240
288
  prompts_1.log.info("");
241
289
  prompts_1.log.info(" To finish setup:");
242
290
  prompts_1.log.info("");
243
- prompts_1.log.info(` 1. Get your Clerk keys from ${picocolors_1.default.cyan("https://dashboard.clerk.com")}`);
244
- prompts_1.log.info(` 2. Add them to ${picocolors_1.default.cyan(".env.local")}:`);
291
+ prompts_1.log.info(` 1. Create a Clerk app at ${picocolors_1.default.cyan("https://dashboard.clerk.com")}`);
292
+ prompts_1.log.info(` 2. Enable Google and Apple OAuth providers`);
293
+ prompts_1.log.info(` 3. Add keys to ${picocolors_1.default.cyan(".env.local")}:`);
245
294
  prompts_1.log.info(` ${picocolors_1.default.dim("EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...")}`);
246
295
  prompts_1.log.info(` ${picocolors_1.default.dim("CLERK_JWT_ISSUER_DOMAIN=https://your-app.clerk.accounts.dev")}`);
247
- prompts_1.log.info(` 3. Set the Convex env var:`);
296
+ prompts_1.log.info(` 4. Set the Convex env var:`);
248
297
  prompts_1.log.info(` ${picocolors_1.default.dim("bunx convex env set CLERK_JWT_ISSUER_DOMAIN <domain>")}`);
249
- prompts_1.log.info(` 4. Create a JWT template named ${picocolors_1.default.bold('"convex"')} in Clerk dashboard`);
250
- prompts_1.log.info(` 5. Deploy Convex: ${picocolors_1.default.dim("bunx convex dev --once")}`);
298
+ prompts_1.log.info(` 5. Create a JWT template named ${picocolors_1.default.bold('"convex"')} in Clerk dashboard`);
299
+ prompts_1.log.info(` 6. Deploy Convex: ${picocolors_1.default.dim("bunx convex dev --once")}`);
251
300
  prompts_1.log.info("");
252
301
  prompts_1.log.info(" Then start your app:");
253
302
  prompts_1.log.info(` ${picocolors_1.default.cyan("cd")} ${projectName}`);
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.extractDomainFromPublishableKey = extractDomainFromPublishableKey;
37
+ exports.createConvexJwtTemplate = createConvexJwtTemplate;
38
+ const https = __importStar(require("node:https"));
39
+ function clerkApi(secretKey, method, path, body) {
40
+ return new Promise((resolve, reject) => {
41
+ const data = body ? JSON.stringify(body) : undefined;
42
+ const req = https.request({
43
+ hostname: "api.clerk.com",
44
+ path: `/v1${path}`,
45
+ method,
46
+ headers: {
47
+ Authorization: `Bearer ${secretKey}`,
48
+ "Content-Type": "application/json",
49
+ ...(data ? { "Content-Length": String(Buffer.byteLength(data)) } : {}),
50
+ },
51
+ }, (res) => {
52
+ let responseBody = "";
53
+ res.on("data", (chunk) => (responseBody += chunk));
54
+ res.on("end", () => {
55
+ try {
56
+ resolve({ status: res.statusCode ?? 0, data: JSON.parse(responseBody) });
57
+ }
58
+ catch {
59
+ resolve({ status: res.statusCode ?? 0, data: responseBody });
60
+ }
61
+ });
62
+ });
63
+ req.on("error", reject);
64
+ if (data)
65
+ req.write(data);
66
+ req.end();
67
+ });
68
+ }
69
+ /**
70
+ * Extract the JWT issuer domain from a Clerk publishable key.
71
+ * Clerk keys encode the Frontend API domain in base64 after the prefix.
72
+ */
73
+ function extractDomainFromPublishableKey(key) {
74
+ try {
75
+ const prefix = key.startsWith("pk_test_")
76
+ ? "pk_test_"
77
+ : key.startsWith("pk_live_")
78
+ ? "pk_live_"
79
+ : null;
80
+ if (!prefix)
81
+ return null;
82
+ const base64Part = key.slice(prefix.length);
83
+ const decoded = Buffer.from(base64Part, "base64").toString("utf-8");
84
+ // Remove trailing $ if present
85
+ const domain = decoded.replace(/\$$/, "");
86
+ if (domain.includes("clerk")) {
87
+ return `https://${domain}`;
88
+ }
89
+ return null;
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ }
95
+ /**
96
+ * Create a "convex" JWT template in Clerk via the Backend API.
97
+ * Returns success even if the template already exists.
98
+ */
99
+ async function createConvexJwtTemplate(secretKey) {
100
+ try {
101
+ const res = await clerkApi(secretKey, "POST", "/jwt_templates", {
102
+ name: "convex",
103
+ claims: {},
104
+ lifetime: 60,
105
+ allowed_clock_skew: 5,
106
+ });
107
+ if (res.status >= 200 && res.status < 300) {
108
+ return { success: true };
109
+ }
110
+ // Template already exists — that's fine
111
+ if (res.status === 422 || res.status === 409) {
112
+ return { success: true };
113
+ }
114
+ const message = res.data?.errors?.[0]?.long_message ||
115
+ res.data?.errors?.[0]?.message ||
116
+ `HTTP ${res.status}`;
117
+ return { success: false, error: message };
118
+ }
119
+ catch (err) {
120
+ return {
121
+ success: false,
122
+ error: err instanceof Error ? err.message : "Network error",
123
+ };
124
+ }
125
+ }
@@ -6,21 +6,8 @@ function authLayoutTemplate() {
6
6
 
7
7
  export default function AuthLayout() {
8
8
  return (
9
- <Stack>
10
- <Stack.Screen
11
- name="sign-in"
12
- options={{
13
- title: "Sign In",
14
- headerBackTitle: "Back",
15
- }}
16
- />
17
- <Stack.Screen
18
- name="sign-up"
19
- options={{
20
- title: "Sign Up",
21
- headerBackTitle: "Back",
22
- }}
23
- />
9
+ <Stack screenOptions={{ headerShown: false }}>
10
+ <Stack.Screen name="sign-in" />
24
11
  </Stack>
25
12
  );
26
13
  }
@@ -13,6 +13,8 @@ export default function RootLayout() {
13
13
  return (
14
14
  <AuthProvider>
15
15
  <Stack screenOptions={{ headerShown: false }}>
16
+ <Stack.Screen name="(tabs)" />
17
+ <Stack.Screen name="(auth)" />
16
18
  <Stack.Screen name="+not-found" />
17
19
  </Stack>
18
20
  <StatusBar style="auto" />
@@ -32,17 +32,11 @@ export default function SettingsScreen() {
32
32
  const handleSignOut = async () => {
33
33
  if (Platform.OS === "web") {
34
34
  const confirmed = window.confirm("Are you sure you want to sign out?");
35
- if (confirmed) {
36
- await performSignOut();
37
- }
35
+ if (confirmed) await performSignOut();
38
36
  } else {
39
37
  Alert.alert("Sign Out", "Are you sure you want to sign out?", [
40
38
  { text: "Cancel", style: "cancel" },
41
- {
42
- text: "Sign Out",
43
- style: "destructive",
44
- onPress: performSignOut,
45
- },
39
+ { text: "Sign Out", style: "destructive", onPress: performSignOut },
46
40
  ]);
47
41
  }
48
42
  };
@@ -56,18 +50,12 @@ export default function SettingsScreen() {
56
50
  SecureStore.deleteItemAsync("__clerk_db_jwt"),
57
51
  SecureStore.deleteItemAsync("__clerk_session_jwt"),
58
52
  ]);
59
- } catch {
60
- // Token cleanup errors are ignorable
61
- }
53
+ } catch {}
62
54
 
63
55
  await signOut();
64
56
  } catch (error) {
65
57
  console.error("Sign out error:", error);
66
- if (Platform.OS === "web") {
67
- window.alert("Failed to sign out. Please try again.");
68
- } else {
69
- Alert.alert("Error", "Failed to sign out. Please try again.");
70
- }
58
+ Alert.alert("Error", "Failed to sign out. Please try again.");
71
59
  } finally {
72
60
  setIsSigningOut(false);
73
61
  }
@@ -125,7 +113,7 @@ export default function SettingsScreen() {
125
113
  <View style={styles.section}>
126
114
  <Text style={styles.sectionTitle}>Account</Text>
127
115
 
128
- <TouchableOpacity style={styles.menuItem}>
116
+ <TouchableOpacity style={styles.menuItem} activeOpacity={0.7}>
129
117
  <Shield size={20} color="#6B7280" strokeWidth={2} />
130
118
  <Text style={styles.menuText}>Privacy & Security</Text>
131
119
  </TouchableOpacity>
@@ -158,9 +146,6 @@ export default function SettingsScreen() {
158
146
  <Text style={styles.sectionText}>
159
147
  Built with Expo, Convex, and Clerk
160
148
  </Text>
161
- <Text style={styles.sectionText}>
162
- Real-time sync across all your devices
163
- </Text>
164
149
  </View>
165
150
  </View>
166
151
  </SafeAreaView>
@@ -178,9 +163,9 @@ const styles = StyleSheet.create({
178
163
  alignItems: "center",
179
164
  },
180
165
  header: {
181
- paddingHorizontal: 24,
182
- paddingTop: 20,
183
- paddingBottom: 24,
166
+ paddingHorizontal: 16,
167
+ paddingTop: 24,
168
+ paddingBottom: 16,
184
169
  },
185
170
  title: {
186
171
  fontSize: 28,
@@ -189,32 +174,32 @@ const styles = StyleSheet.create({
189
174
  },
190
175
  content: {
191
176
  flex: 1,
192
- paddingHorizontal: 24,
193
- paddingBottom: 100,
177
+ paddingHorizontal: 16,
178
+ paddingBottom: 96,
194
179
  },
195
180
  profileSection: {
196
181
  marginBottom: 32,
197
182
  },
198
183
  profileCard: {
199
184
  backgroundColor: "#F8F9FA",
200
- borderRadius: 12,
185
+ borderRadius: 16,
201
186
  padding: 16,
202
187
  flexDirection: "row",
203
188
  alignItems: "center",
204
189
  },
205
190
  avatarContainer: {
206
- width: 60,
207
- height: 60,
208
- borderRadius: 30,
191
+ width: 56,
192
+ height: 56,
193
+ borderRadius: 28,
209
194
  backgroundColor: "#E5E7EB",
210
195
  alignItems: "center",
211
196
  justifyContent: "center",
212
197
  marginRight: 16,
213
198
  },
214
199
  avatar: {
215
- width: 60,
216
- height: 60,
217
- borderRadius: 30,
200
+ width: 56,
201
+ height: 56,
202
+ borderRadius: 28,
218
203
  },
219
204
  profileInfo: {
220
205
  flex: 1,
@@ -232,7 +217,7 @@ const styles = StyleSheet.create({
232
217
  profileEmail: {
233
218
  fontSize: 14,
234
219
  color: "#6B7280",
235
- marginLeft: 6,
220
+ marginLeft: 8,
236
221
  },
237
222
  section: {
238
223
  marginBottom: 24,
@@ -251,10 +236,11 @@ const styles = StyleSheet.create({
251
236
  menuItem: {
252
237
  flexDirection: "row",
253
238
  alignItems: "center",
239
+ minHeight: 48,
254
240
  paddingVertical: 12,
255
241
  paddingHorizontal: 16,
256
242
  backgroundColor: "#F8F9FA",
257
- borderRadius: 8,
243
+ borderRadius: 12,
258
244
  marginBottom: 8,
259
245
  },
260
246
  menuText: {
@@ -6,195 +6,203 @@ function signInTemplate() {
6
6
  import {
7
7
  View,
8
8
  Text,
9
- TextInput,
10
9
  TouchableOpacity,
11
10
  StyleSheet,
12
- KeyboardAvoidingView,
13
- Platform,
14
11
  Alert,
15
12
  ActivityIndicator,
13
+ Platform,
16
14
  } from "react-native";
17
- import { useSignIn } from "@clerk/clerk-expo";
18
- import { useRouter, Link } from "expo-router";
15
+ import { SafeAreaView } from "react-native-safe-area-context";
16
+ import { useOAuth } from "@clerk/clerk-expo";
17
+ import { useRouter } from "expo-router";
18
+ import { SquareCheck as CheckSquare } from "lucide-react-native";
19
+ import * as WebBrowser from "expo-web-browser";
20
+
21
+ WebBrowser.maybeCompleteAuthSession();
19
22
 
20
23
  export default function SignInScreen() {
21
- const { signIn, setActive, isLoaded } = useSignIn();
22
24
  const router = useRouter();
25
+ const { startOAuthFlow: startGoogleOAuth } = useOAuth({
26
+ strategy: "oauth_google",
27
+ });
28
+ const { startOAuthFlow: startAppleOAuth } = useOAuth({
29
+ strategy: "oauth_apple",
30
+ });
23
31
 
24
- const [emailAddress, setEmailAddress] = useState("");
25
- const [password, setPassword] = useState("");
26
- const [loading, setLoading] = useState(false);
27
- const [errorMessage, setErrorMessage] = useState("");
32
+ const [loading, setLoading] = useState<"google" | "apple" | null>(null);
28
33
 
29
- const onSignInPress = async () => {
30
- if (!isLoaded) return;
34
+ const handleOAuth = async (
35
+ provider: "google" | "apple",
36
+ startFlow: typeof startGoogleOAuth
37
+ ) => {
38
+ if (loading) return;
39
+ setLoading(provider);
31
40
 
32
- setLoading(true);
33
- setErrorMessage("");
34
41
  try {
35
- const signInAttempt = await signIn.create({
36
- identifier: emailAddress,
37
- password,
38
- });
42
+ const { createdSessionId, setActive } = await startFlow();
39
43
 
40
- if (signInAttempt.status === "complete") {
41
- await setActive({ session: signInAttempt.createdSessionId });
44
+ if (createdSessionId && setActive) {
45
+ await setActive({ session: createdSessionId });
42
46
  router.replace("/(tabs)");
43
- } else {
44
- setErrorMessage("Sign in failed. Please try again.");
45
47
  }
46
48
  } catch (err: any) {
47
- const errorCode = err.errors?.[0]?.code;
48
- let message = "Sign in failed";
49
-
50
- if (errorCode === "form_identifier_not_found") {
51
- message = "No account found with this email.";
52
- } else if (errorCode === "form_password_incorrect") {
53
- message = "Incorrect password. Please try again.";
54
- } else if (err.errors?.[0]?.message) {
55
- message = err.errors[0].message;
56
- }
57
-
58
- setErrorMessage(message);
49
+ console.error(\`\${provider} OAuth error:\`, err);
50
+ const message =
51
+ err?.errors?.[0]?.message || "Sign in failed. Please try again.";
59
52
  Alert.alert("Sign In Failed", message);
60
53
  } finally {
61
- setLoading(false);
54
+ setLoading(null);
62
55
  }
63
56
  };
64
57
 
65
58
  return (
66
- <KeyboardAvoidingView
67
- behavior={Platform.OS === "ios" ? "padding" : "height"}
68
- style={styles.container}
69
- >
59
+ <SafeAreaView style={styles.container}>
70
60
  <View style={styles.content}>
71
- <Text style={styles.title}>Welcome Back</Text>
72
- <Text style={styles.subtitle}>Sign in to continue</Text>
73
-
74
- {errorMessage ? (
75
- <View style={styles.errorContainer}>
76
- <Text style={styles.errorText}>{errorMessage}</Text>
61
+ <View style={styles.branding}>
62
+ <View style={styles.logoContainer}>
63
+ <CheckSquare size={40} color="#1F2937" strokeWidth={2} />
77
64
  </View>
78
- ) : null}
79
-
80
- <TextInput
81
- style={styles.input}
82
- placeholder="Email"
83
- placeholderTextColor="#999"
84
- value={emailAddress}
85
- onChangeText={setEmailAddress}
86
- autoCapitalize="none"
87
- keyboardType="email-address"
88
- />
89
-
90
- <TextInput
91
- style={styles.input}
92
- placeholder="Password"
93
- placeholderTextColor="#999"
94
- value={password}
95
- onChangeText={setPassword}
96
- secureTextEntry
97
- />
65
+ <Text style={styles.title}>TaskSync</Text>
66
+ <Text style={styles.subtitle}>
67
+ Your tasks, synced across all devices
68
+ </Text>
69
+ </View>
98
70
 
99
- <TouchableOpacity
100
- style={[styles.button, loading && styles.buttonDisabled]}
101
- onPress={onSignInPress}
102
- disabled={loading}
103
- >
104
- {loading ? (
105
- <ActivityIndicator color="#fff" />
106
- ) : (
107
- <Text style={styles.buttonText}>Sign In</Text>
108
- )}
109
- </TouchableOpacity>
71
+ <View style={styles.buttons}>
72
+ <TouchableOpacity
73
+ style={[
74
+ styles.button,
75
+ styles.googleButton,
76
+ loading === "google" && styles.buttonLoading,
77
+ ]}
78
+ onPress={() => handleOAuth("google", startGoogleOAuth)}
79
+ activeOpacity={0.7}
80
+ disabled={loading !== null}
81
+ >
82
+ {loading === "google" ? (
83
+ <ActivityIndicator size={20} color="#1F2937" />
84
+ ) : (
85
+ <>
86
+ <Text style={styles.googleIcon}>G</Text>
87
+ <Text style={styles.googleText}>Continue with Google</Text>
88
+ </>
89
+ )}
90
+ </TouchableOpacity>
110
91
 
111
- <View style={styles.footer}>
112
- <Text style={styles.footerText}>Don't have an account? </Text>
113
- <Link href="/(auth)/sign-up" asChild>
114
- <TouchableOpacity>
115
- <Text style={styles.link}>Sign Up</Text>
116
- </TouchableOpacity>
117
- </Link>
92
+ <TouchableOpacity
93
+ style={[
94
+ styles.button,
95
+ styles.appleButton,
96
+ loading === "apple" && styles.buttonLoading,
97
+ ]}
98
+ onPress={() => handleOAuth("apple", startAppleOAuth)}
99
+ activeOpacity={0.7}
100
+ disabled={loading !== null}
101
+ >
102
+ {loading === "apple" ? (
103
+ <ActivityIndicator size={20} color="#FFFFFF" />
104
+ ) : (
105
+ <>
106
+ <Text style={styles.appleIcon}>\u{F8FF}</Text>
107
+ <Text style={styles.appleText}>Continue with Apple</Text>
108
+ </>
109
+ )}
110
+ </TouchableOpacity>
118
111
  </View>
112
+
113
+ <Text style={styles.terms}>
114
+ By continuing, you agree to our Terms of Service and Privacy Policy
115
+ </Text>
119
116
  </View>
120
- </KeyboardAvoidingView>
117
+ </SafeAreaView>
121
118
  );
122
119
  }
123
120
 
124
121
  const styles = StyleSheet.create({
125
122
  container: {
126
123
  flex: 1,
127
- backgroundColor: "#f5f5f5",
124
+ backgroundColor: "#FFFFFF",
128
125
  },
129
126
  content: {
130
127
  flex: 1,
131
128
  justifyContent: "center",
132
- paddingHorizontal: 20,
129
+ paddingHorizontal: 24,
130
+ },
131
+ branding: {
132
+ alignItems: "center",
133
+ marginBottom: 64,
134
+ },
135
+ logoContainer: {
136
+ width: 80,
137
+ height: 80,
138
+ borderRadius: 20,
139
+ backgroundColor: "#F3F4F6",
140
+ alignItems: "center",
141
+ justifyContent: "center",
142
+ marginBottom: 24,
133
143
  },
134
144
  title: {
135
145
  fontSize: 32,
136
- fontWeight: "bold",
137
- color: "#333",
146
+ fontWeight: "700",
147
+ color: "#1F2937",
138
148
  marginBottom: 8,
139
- textAlign: "center",
140
149
  },
141
150
  subtitle: {
142
151
  fontSize: 16,
143
- color: "#666",
144
- marginBottom: 40,
152
+ color: "#6B7280",
145
153
  textAlign: "center",
154
+ lineHeight: 24,
146
155
  },
147
- input: {
148
- backgroundColor: "#fff",
149
- borderRadius: 8,
150
- paddingHorizontal: 16,
151
- paddingVertical: 12,
152
- fontSize: 16,
153
- marginBottom: 16,
154
- borderWidth: 1,
155
- borderColor: "#e0e0e0",
156
+ buttons: {
157
+ gap: 12,
156
158
  },
157
159
  button: {
158
- backgroundColor: "#007AFF",
159
- borderRadius: 8,
160
- paddingVertical: 14,
160
+ flexDirection: "row",
161
161
  alignItems: "center",
162
- marginTop: 8,
162
+ justifyContent: "center",
163
+ height: 56,
164
+ borderRadius: 12,
165
+ paddingHorizontal: 24,
163
166
  },
164
- buttonDisabled: {
167
+ buttonLoading: {
165
168
  opacity: 0.7,
166
169
  },
167
- buttonText: {
168
- color: "#fff",
169
- fontSize: 18,
170
+ googleButton: {
171
+ backgroundColor: "#FFFFFF",
172
+ borderWidth: 1.5,
173
+ borderColor: "#E5E7EB",
174
+ },
175
+ googleIcon: {
176
+ fontSize: 20,
177
+ fontWeight: "700",
178
+ color: "#4285F4",
179
+ marginRight: 12,
180
+ },
181
+ googleText: {
182
+ fontSize: 16,
170
183
  fontWeight: "600",
184
+ color: "#1F2937",
171
185
  },
172
- footer: {
173
- flexDirection: "row",
174
- justifyContent: "center",
175
- marginTop: 24,
186
+ appleButton: {
187
+ backgroundColor: "#000000",
176
188
  },
177
- footerText: {
178
- color: "#666",
179
- fontSize: 14,
189
+ appleIcon: {
190
+ fontSize: 20,
191
+ color: "#FFFFFF",
192
+ marginRight: 12,
180
193
  },
181
- link: {
182
- color: "#007AFF",
183
- fontSize: 14,
194
+ appleText: {
195
+ fontSize: 16,
184
196
  fontWeight: "600",
197
+ color: "#FFFFFF",
185
198
  },
186
- errorContainer: {
187
- backgroundColor: "#ffebee",
188
- borderRadius: 8,
189
- padding: 12,
190
- marginBottom: 16,
191
- borderWidth: 1,
192
- borderColor: "#ef5350",
193
- },
194
- errorText: {
195
- color: "#c62828",
196
- fontSize: 14,
199
+ terms: {
200
+ fontSize: 13,
201
+ color: "#9CA3AF",
197
202
  textAlign: "center",
203
+ marginTop: 32,
204
+ lineHeight: 20,
205
+ paddingHorizontal: 16,
198
206
  },
199
207
  });
200
208
  `;
@@ -27,10 +27,9 @@ export default function TabLayout() {
27
27
  borderTopWidth: 0,
28
28
  elevation: 0,
29
29
  shadowOpacity: 0,
30
- height: Platform.OS === "ios" ? 90 : 70,
31
- paddingBottom: Platform.OS === "ios" ? 30 : 12,
30
+ height: Platform.OS === "ios" ? 88 : 72,
31
+ paddingBottom: Platform.OS === "ios" ? 32 : 12,
32
32
  paddingTop: 12,
33
- borderTopColor: "transparent",
34
33
  },
35
34
  tabBarActiveTintColor: "#1F2937",
36
35
  tabBarInactiveTintColor: "#9CA3AF",
@@ -38,10 +37,6 @@ export default function TabLayout() {
38
37
  fontSize: 13,
39
38
  fontWeight: "500",
40
39
  marginTop: 4,
41
- marginBottom: Platform.OS === "ios" ? 0 : 2,
42
- },
43
- tabBarIconStyle: {
44
- marginBottom: Platform.OS === "ios" ? 2 : 0,
45
40
  },
46
41
  }}
47
42
  >
@@ -3,13 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.todosScreenTemplate = todosScreenTemplate;
4
4
  function todosScreenTemplate() {
5
5
  return `import React, { useState } from "react";
6
- import {
7
- View,
8
- Text,
9
- ScrollView,
10
- StyleSheet,
11
- StatusBar,
12
- } from "react-native";
6
+ import { View, Text, ScrollView, StyleSheet, StatusBar } from "react-native";
13
7
  import { SafeAreaView } from "react-native-safe-area-context";
14
8
  import { AddTodoForm } from "@/components/AddTodoForm";
15
9
  import { TodoItem } from "@/components/TodoItem";
@@ -130,9 +124,9 @@ const styles = StyleSheet.create({
130
124
  backgroundColor: "#FFFFFF",
131
125
  },
132
126
  header: {
133
- paddingHorizontal: 24,
134
- paddingTop: 20,
135
- paddingBottom: 24,
127
+ paddingHorizontal: 16,
128
+ paddingTop: 24,
129
+ paddingBottom: 16,
136
130
  },
137
131
  title: {
138
132
  fontSize: 28,
@@ -147,10 +141,10 @@ const styles = StyleSheet.create({
147
141
  },
148
142
  todosContainer: {
149
143
  flex: 1,
150
- paddingHorizontal: 24,
144
+ paddingHorizontal: 16,
151
145
  },
152
146
  todosContent: {
153
- paddingBottom: 100,
147
+ paddingBottom: 96,
154
148
  flexGrow: 1,
155
149
  },
156
150
  });
@@ -37,11 +37,12 @@ export function AddTodoForm({ onAddTodo }: AddTodoFormProps) {
37
37
  style={[styles.addButton, !text.trim() && styles.addButtonDisabled]}
38
38
  onPress={handleSubmit}
39
39
  disabled={!text.trim()}
40
+ activeOpacity={0.7}
40
41
  >
41
42
  <Plus
42
43
  size={20}
43
44
  color={text.trim() ? "#FFFFFF" : "#9CA3AF"}
44
- strokeWidth={2}
45
+ strokeWidth={2.5}
45
46
  />
46
47
  </TouchableOpacity>
47
48
  </View>
@@ -51,8 +52,8 @@ export function AddTodoForm({ onAddTodo }: AddTodoFormProps) {
51
52
 
52
53
  const styles = StyleSheet.create({
53
54
  container: {
54
- paddingHorizontal: 24,
55
- marginBottom: 24,
55
+ paddingHorizontal: 16,
56
+ marginBottom: 16,
56
57
  },
57
58
  inputContainer: {
58
59
  flexDirection: "row",
@@ -61,21 +62,22 @@ const styles = StyleSheet.create({
61
62
  borderRadius: 12,
62
63
  borderWidth: 1,
63
64
  borderColor: "#E5E7EB",
64
- paddingHorizontal: 16,
65
- paddingVertical: 4,
65
+ paddingLeft: 16,
66
+ paddingRight: 8,
67
+ minHeight: 56,
66
68
  },
67
69
  input: {
68
70
  flex: 1,
69
71
  fontSize: 16,
70
72
  color: "#1F2937",
71
- paddingVertical: 12,
73
+ paddingVertical: 16,
72
74
  paddingRight: 12,
73
75
  },
74
76
  addButton: {
75
77
  backgroundColor: "#1F2937",
76
- borderRadius: 8,
77
- width: 32,
78
- height: 32,
78
+ borderRadius: 10,
79
+ width: 40,
80
+ height: 40,
79
81
  alignItems: "center",
80
82
  justifyContent: "center",
81
83
  },
@@ -17,33 +17,30 @@ interface EmptyStateProps {
17
17
  }
18
18
 
19
19
  export function EmptyState({ filter }: EmptyStateProps) {
20
- const getEmptyStateContent = () => {
20
+ const getContent = () => {
21
21
  switch (filter) {
22
22
  case "active":
23
23
  return {
24
24
  icon: <CheckCircle size={48} color="#D1D5DB" strokeWidth={1.5} />,
25
25
  title: "All caught up!",
26
- description:
27
- "You have no active tasks. Time to add some new ones or take a well-deserved break.",
26
+ description: "No active tasks. Add some new ones or take a break.",
28
27
  };
29
28
  case "completed":
30
29
  return {
31
30
  icon: <Circle size={48} color="#D1D5DB" strokeWidth={1.5} />,
32
31
  title: "No completed tasks",
33
- description:
34
- "Tasks you complete will appear here. Start checking off some items!",
32
+ description: "Tasks you complete will appear here.",
35
33
  };
36
34
  default:
37
35
  return {
38
36
  icon: <ListTodo size={48} color="#D1D5DB" strokeWidth={1.5} />,
39
37
  title: "No tasks yet",
40
- description:
41
- "Add your first task above to get started with organizing your day.",
38
+ description: "Add your first task above to get started.",
42
39
  };
43
40
  }
44
41
  };
45
42
 
46
- const content = getEmptyStateContent();
43
+ const content = getContent();
47
44
 
48
45
  return (
49
46
  <View style={styles.container}>
@@ -59,7 +56,7 @@ const styles = StyleSheet.create({
59
56
  flex: 1,
60
57
  alignItems: "center",
61
58
  justifyContent: "center",
62
- paddingHorizontal: 40,
59
+ paddingHorizontal: 48,
63
60
  paddingVertical: 80,
64
61
  },
65
62
  iconContainer: {
@@ -55,11 +55,10 @@ export function FilterTabs({ filter, onFilterChange, counts }: FilterTabsProps)
55
55
  const styles = StyleSheet.create({
56
56
  container: {
57
57
  flexDirection: "row",
58
- paddingHorizontal: 24,
59
58
  marginBottom: 16,
60
- backgroundColor: "#F8F9FA",
59
+ backgroundColor: "#F3F4F6",
61
60
  borderRadius: 12,
62
- marginHorizontal: 24,
61
+ marginHorizontal: 16,
63
62
  padding: 4,
64
63
  },
65
64
  tab: {
@@ -70,37 +69,37 @@ const styles = StyleSheet.create({
70
69
  paddingVertical: 12,
71
70
  paddingHorizontal: 8,
72
71
  borderRadius: 8,
73
- minHeight: 40,
72
+ minHeight: 48,
74
73
  },
75
74
  tabActive: {
76
75
  backgroundColor: "#FFFFFF",
77
76
  shadowColor: "#000000",
78
77
  shadowOffset: { width: 0, height: 1 },
79
- shadowOpacity: 0.05,
80
- shadowRadius: 2,
81
- elevation: 1,
78
+ shadowOpacity: 0.06,
79
+ shadowRadius: 3,
80
+ elevation: 2,
82
81
  },
83
82
  tabText: {
84
- fontSize: 13,
83
+ fontSize: 14,
85
84
  fontWeight: "500",
86
85
  color: "#6B7280",
87
86
  marginRight: 4,
88
- flexShrink: 0,
89
87
  },
90
88
  tabTextActive: {
91
89
  color: "#1F2937",
90
+ fontWeight: "600",
92
91
  },
93
92
  tabCount: {
94
- fontSize: 11,
93
+ fontSize: 12,
95
94
  fontWeight: "600",
96
95
  color: "#9CA3AF",
97
96
  backgroundColor: "#E5E7EB",
98
97
  borderRadius: 10,
99
- paddingHorizontal: 5,
100
- paddingVertical: 1,
101
- minWidth: 18,
98
+ paddingHorizontal: 6,
99
+ paddingVertical: 2,
100
+ minWidth: 20,
102
101
  textAlign: "center",
103
- flexShrink: 0,
102
+ overflow: "hidden",
104
103
  },
105
104
  tabCountActive: {
106
105
  color: "#1F2937",
@@ -18,7 +18,7 @@ export function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
18
18
  return (
19
19
  <View style={styles.container}>
20
20
  <TouchableOpacity
21
- style={styles.checkboxContainer}
21
+ style={styles.checkboxHitArea}
22
22
  onPress={() => onToggle(todo._id)}
23
23
  activeOpacity={0.7}
24
24
  >
@@ -26,7 +26,7 @@ export function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
26
26
  style={[styles.checkbox, todo.completed && styles.checkboxCompleted]}
27
27
  >
28
28
  {todo.completed && (
29
- <Check size={16} color="#FFFFFF" strokeWidth={2.5} />
29
+ <Check size={14} color="#FFFFFF" strokeWidth={3} />
30
30
  )}
31
31
  </View>
32
32
  </TouchableOpacity>
@@ -40,7 +40,7 @@ export function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
40
40
  </View>
41
41
 
42
42
  <TouchableOpacity
43
- style={styles.deleteButton}
43
+ style={styles.deleteHitArea}
44
44
  onPress={() => onDelete(todo._id)}
45
45
  activeOpacity={0.7}
46
46
  >
@@ -56,19 +56,24 @@ const styles = StyleSheet.create({
56
56
  borderRadius: 12,
57
57
  borderWidth: 1,
58
58
  borderColor: "#E5E7EB",
59
- paddingHorizontal: 16,
60
- paddingVertical: 16,
59
+ paddingLeft: 4,
60
+ paddingRight: 4,
61
+ paddingVertical: 4,
61
62
  marginBottom: 8,
62
63
  flexDirection: "row",
63
64
  alignItems: "center",
65
+ minHeight: 56,
64
66
  },
65
- checkboxContainer: {
66
- marginRight: 12,
67
+ checkboxHitArea: {
68
+ width: 48,
69
+ height: 48,
70
+ alignItems: "center",
71
+ justifyContent: "center",
67
72
  },
68
73
  checkbox: {
69
- width: 20,
70
- height: 20,
71
- borderRadius: 4,
74
+ width: 24,
75
+ height: 24,
76
+ borderRadius: 6,
72
77
  borderWidth: 2,
73
78
  borderColor: "#D1D5DB",
74
79
  alignItems: "center",
@@ -80,7 +85,7 @@ const styles = StyleSheet.create({
80
85
  },
81
86
  content: {
82
87
  flex: 1,
83
- marginRight: 12,
88
+ paddingVertical: 12,
84
89
  },
85
90
  text: {
86
91
  fontSize: 16,
@@ -91,8 +96,11 @@ const styles = StyleSheet.create({
91
96
  color: "#9CA3AF",
92
97
  textDecorationLine: "line-through",
93
98
  },
94
- deleteButton: {
95
- padding: 4,
99
+ deleteHitArea: {
100
+ width: 48,
101
+ height: 48,
102
+ alignItems: "center",
103
+ justifyContent: "center",
96
104
  },
97
105
  });
98
106
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-100x-mobile",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Scaffold a full-stack mobile app with Expo + Convex + Clerk in seconds",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {