create-100x-mobile 0.5.3 → 0.6.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/commands/new/steps.js +4 -1
- package/dist/commands/new.js +22 -1
- package/dist/templates/cloudflare/cloudflareStorage.js +17 -0
- package/dist/templates/cloudflare/cloudflareTabsLayout.js +65 -0
- package/dist/templates/cloudflare/filesScreen.js +499 -0
- package/dist/templates/config/agentsMd.js +406 -0
- package/dist/templates/config/packageJson.js +4 -1
- package/package.json +1 -1
|
@@ -249,7 +249,10 @@ function buildExpoInstallPackages(backend, instantAuthMode = "clerk") {
|
|
|
249
249
|
"@react-native-async-storage/async-storage",
|
|
250
250
|
"@react-native-community/netinfo",
|
|
251
251
|
];
|
|
252
|
-
|
|
252
|
+
const cloudflareExpoPackages = (0, scaffold_1.isCloudflareInstantAuthMode)(instantAuthMode)
|
|
253
|
+
? ["expo-document-picker"]
|
|
254
|
+
: [];
|
|
255
|
+
return [...commonExpoPackages, ...backendExpoPackages, ...cloudflareExpoPackages];
|
|
253
256
|
}
|
|
254
257
|
async function resolveExpoDependencies(projectDir, backend, instantAuthMode = "clerk") {
|
|
255
258
|
const expoPackages = buildExpoInstallPackages(backend, instantAuthMode);
|
package/dist/commands/new.js
CHANGED
|
@@ -51,6 +51,8 @@ const tabsLayout_3 = require("../templates/instant/magic/tabsLayout");
|
|
|
51
51
|
// Cloudflare templates
|
|
52
52
|
const alchemyRun_1 = require("../templates/cloudflare/alchemyRun");
|
|
53
53
|
const cloudflareStorage_1 = require("../templates/cloudflare/cloudflareStorage");
|
|
54
|
+
const cloudflareTabsLayout_1 = require("../templates/cloudflare/cloudflareTabsLayout");
|
|
55
|
+
const filesScreen_1 = require("../templates/cloudflare/filesScreen");
|
|
54
56
|
const instantCloudflareLib_1 = require("../templates/cloudflare/instantCloudflareLib");
|
|
55
57
|
const worker_1 = require("../templates/cloudflare/worker");
|
|
56
58
|
const workerMigration_1 = require("../templates/cloudflare/workerMigration");
|
|
@@ -62,6 +64,8 @@ const filterTabs_1 = require("../templates/components/filterTabs");
|
|
|
62
64
|
const todoItem_1 = require("../templates/components/todoItem");
|
|
63
65
|
// Hook templates
|
|
64
66
|
const useFrameworkReady_1 = require("../templates/hooks/useFrameworkReady");
|
|
67
|
+
// AGENTS.md template
|
|
68
|
+
const agentsMd_1 = require("../templates/config/agentsMd");
|
|
65
69
|
function buildTemplateFiles(projectName, expoVersion, backend, instantAuthMode = "clerk") {
|
|
66
70
|
if (backend === "instantdb") {
|
|
67
71
|
const cloudflareFiles = (0, scaffold_1.isCloudflareInstantAuthMode)(instantAuthMode)
|
|
@@ -95,12 +99,24 @@ function buildTemplateFiles(projectName, expoVersion, backend, instantAuthMode =
|
|
|
95
99
|
"README.md",
|
|
96
100
|
(0, readme_1.readmeTemplate)(projectName, backend, instantAuthMode),
|
|
97
101
|
],
|
|
102
|
+
[
|
|
103
|
+
"AGENTS.md",
|
|
104
|
+
(0, agentsMd_1.agentsMdTemplate)(projectName, backend, instantAuthMode),
|
|
105
|
+
],
|
|
98
106
|
// Instant app
|
|
99
107
|
["app/_layout.tsx", (0, rootLayout_3.magicRootLayoutTemplate)()],
|
|
100
108
|
["app/+not-found.tsx", (0, notFound_1.notFoundTemplate)()],
|
|
101
109
|
["app/(auth)/_layout.tsx", (0, authLayout_2.magicAuthLayoutTemplate)()],
|
|
102
110
|
["app/(auth)/sign-in.tsx", (0, signIn_2.magicSignInTemplate)()],
|
|
103
|
-
[
|
|
111
|
+
[
|
|
112
|
+
"app/(tabs)/_layout.tsx",
|
|
113
|
+
(0, scaffold_1.isCloudflareInstantAuthMode)(instantAuthMode)
|
|
114
|
+
? (0, cloudflareTabsLayout_1.cloudflareTabsLayoutTemplate)()
|
|
115
|
+
: (0, tabsLayout_3.magicTabsLayoutTemplate)(),
|
|
116
|
+
],
|
|
117
|
+
...((0, scaffold_1.isCloudflareInstantAuthMode)(instantAuthMode)
|
|
118
|
+
? [["app/(tabs)/files.tsx", (0, filesScreen_1.filesScreenTemplate)()]]
|
|
119
|
+
: []),
|
|
104
120
|
["app/(tabs)/index.tsx", (0, todosScreen_2.instantTodosScreenTemplate)()],
|
|
105
121
|
[
|
|
106
122
|
"app/(tabs)/settings.tsx",
|
|
@@ -135,6 +151,10 @@ function buildTemplateFiles(projectName, expoVersion, backend, instantAuthMode =
|
|
|
135
151
|
"README.md",
|
|
136
152
|
(0, readme_1.readmeTemplate)(projectName, backend, instantAuthMode),
|
|
137
153
|
],
|
|
154
|
+
[
|
|
155
|
+
"AGENTS.md",
|
|
156
|
+
(0, agentsMd_1.agentsMdTemplate)(projectName, backend, instantAuthMode),
|
|
157
|
+
],
|
|
138
158
|
// Instant app
|
|
139
159
|
["app/_layout.tsx", (0, rootLayout_2.instantRootLayoutTemplate)()],
|
|
140
160
|
["app/+not-found.tsx", (0, notFound_1.notFoundTemplate)()],
|
|
@@ -161,6 +181,7 @@ function buildTemplateFiles(projectName, expoVersion, backend, instantAuthMode =
|
|
|
161
181
|
[".prettierrc", (0, prettierrc_1.prettierrcTemplate)()],
|
|
162
182
|
["eas.json", (0, easJson_1.easJsonTemplate)()],
|
|
163
183
|
["README.md", (0, readme_1.readmeTemplate)(projectName, backend)],
|
|
184
|
+
["AGENTS.md", (0, agentsMd_1.agentsMdTemplate)(projectName, backend)],
|
|
164
185
|
// Convex
|
|
165
186
|
["convex/schema.ts", (0, schema_1.schemaTemplate)()],
|
|
166
187
|
["convex/todos.ts", (0, todos_1.todosTemplate)()],
|
|
@@ -104,5 +104,22 @@ export async function syncCloudflareUploadToInstant(
|
|
|
104
104
|
requireStorageWorkerUrl();
|
|
105
105
|
return input.upload.id;
|
|
106
106
|
}
|
|
107
|
+
|
|
108
|
+
export async function deleteCloudflareObject(
|
|
109
|
+
uploadId: string,
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
const workerUrl = requireStorageWorkerUrl();
|
|
112
|
+
const token = await requireInstantRefreshToken();
|
|
113
|
+
const response = await fetch(\\\`\\\${workerUrl}/uploads/\\\${uploadId}\\\`, {
|
|
114
|
+
method: "DELETE",
|
|
115
|
+
headers: {
|
|
116
|
+
authorization: \\\`Bearer \\\${token}\\\`,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new Error(await readErrorResponse(response));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
107
124
|
`;
|
|
108
125
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cloudflareTabsLayoutTemplate = cloudflareTabsLayoutTemplate;
|
|
4
|
+
function cloudflareTabsLayoutTemplate() {
|
|
5
|
+
return `import { Tabs, Redirect } from "expo-router";
|
|
6
|
+
import { FolderOpen, SquareCheck as CheckSquare, Settings } from "lucide-react-native";
|
|
7
|
+
import { Platform, StyleSheet, Text, View } from "react-native";
|
|
8
|
+
import { instantDb } from "@/lib/instant";
|
|
9
|
+
|
|
10
|
+
function SetupRequired() {
|
|
11
|
+
return (
|
|
12
|
+
<View style={styles.setupContainer}>
|
|
13
|
+
<View style={styles.setupCard}>
|
|
14
|
+
<Text style={styles.setupTitle}>Setup Required</Text>
|
|
15
|
+
<Text style={styles.setupText}>
|
|
16
|
+
Add EXPO_PUBLIC_INSTANT_APP_ID to your .env.local file.
|
|
17
|
+
</Text>
|
|
18
|
+
</View>
|
|
19
|
+
</View>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function AuthenticatedTabsLayout({ db }: { db: NonNullable<typeof instantDb> }) {
|
|
24
|
+
const { isLoading, user, error } = db.useAuth();
|
|
25
|
+
if (isLoading || error) return null;
|
|
26
|
+
if (!user) return <Redirect href="/(auth)/sign-in" />;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Tabs
|
|
30
|
+
screenOptions={{
|
|
31
|
+
headerShown: false,
|
|
32
|
+
tabBarStyle: {
|
|
33
|
+
backgroundColor: "#FFFFFF",
|
|
34
|
+
borderTopWidth: 0,
|
|
35
|
+
elevation: 0,
|
|
36
|
+
shadowOpacity: 0,
|
|
37
|
+
height: Platform.OS === "ios" ? 88 : 72,
|
|
38
|
+
paddingBottom: Platform.OS === "ios" ? 32 : 12,
|
|
39
|
+
paddingTop: 12,
|
|
40
|
+
},
|
|
41
|
+
tabBarActiveTintColor: "#1F2937",
|
|
42
|
+
tabBarInactiveTintColor: "#9CA3AF",
|
|
43
|
+
tabBarLabelStyle: { fontSize: 13, fontWeight: "500", marginTop: 4 },
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<Tabs.Screen name="index" options={{ title: "Todos", tabBarIcon: ({ size, color }) => <CheckSquare size={size} color={color} strokeWidth={2} /> }} />
|
|
47
|
+
<Tabs.Screen name="files" options={{ title: "Files", tabBarIcon: ({ size, color }) => <FolderOpen size={size} color={color} strokeWidth={2} /> }} />
|
|
48
|
+
<Tabs.Screen name="settings" options={{ title: "Settings", tabBarIcon: ({ size, color }) => <Settings size={size} color={color} strokeWidth={2} /> }} />
|
|
49
|
+
</Tabs>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default function TabLayout() {
|
|
54
|
+
if (!instantDb) return <SetupRequired />;
|
|
55
|
+
return <AuthenticatedTabsLayout db={instantDb} />;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const styles = StyleSheet.create({
|
|
59
|
+
setupContainer: { flex: 1, backgroundColor: "#f5f5f5", justifyContent: "center", alignItems: "center", padding: 20 },
|
|
60
|
+
setupCard: { backgroundColor: "#fff", borderRadius: 12, padding: 24, maxWidth: 420, width: "100%", shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 8, elevation: 3 },
|
|
61
|
+
setupTitle: { fontSize: 24, fontWeight: "700", color: "#333", marginBottom: 16, textAlign: "center" },
|
|
62
|
+
setupText: { fontSize: 16, color: "#666", lineHeight: 22, textAlign: "center" },
|
|
63
|
+
});
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.filesScreenTemplate = filesScreenTemplate;
|
|
4
|
+
function filesScreenTemplate() {
|
|
5
|
+
return `import React, { useCallback, useState } from "react";
|
|
6
|
+
import {
|
|
7
|
+
ActivityIndicator,
|
|
8
|
+
Alert,
|
|
9
|
+
FlatList,
|
|
10
|
+
Linking,
|
|
11
|
+
Platform,
|
|
12
|
+
Pressable,
|
|
13
|
+
StatusBar,
|
|
14
|
+
StyleSheet,
|
|
15
|
+
Text,
|
|
16
|
+
View,
|
|
17
|
+
} from "react-native";
|
|
18
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
19
|
+
import {
|
|
20
|
+
Download,
|
|
21
|
+
File,
|
|
22
|
+
FileImage,
|
|
23
|
+
FileText,
|
|
24
|
+
Film,
|
|
25
|
+
Music,
|
|
26
|
+
Plus,
|
|
27
|
+
Trash2,
|
|
28
|
+
Upload,
|
|
29
|
+
} from "lucide-react-native";
|
|
30
|
+
import * as DocumentPicker from "expo-document-picker";
|
|
31
|
+
import {
|
|
32
|
+
instantDb,
|
|
33
|
+
type InstantCloudflareObject,
|
|
34
|
+
} from "@/lib/instant";
|
|
35
|
+
import {
|
|
36
|
+
isCloudflareStorageConfigured,
|
|
37
|
+
uploadCloudflareObject,
|
|
38
|
+
deleteCloudflareObject,
|
|
39
|
+
} from "@/lib/cloudflareStorage";
|
|
40
|
+
|
|
41
|
+
function formatBytes(bytes: number): string {
|
|
42
|
+
if (bytes === 0) return "0 B";
|
|
43
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
44
|
+
const i = Math.min(
|
|
45
|
+
Math.floor(Math.log(bytes) / Math.log(1024)),
|
|
46
|
+
units.length - 1,
|
|
47
|
+
);
|
|
48
|
+
const value = bytes / Math.pow(1024, i);
|
|
49
|
+
return i === 0
|
|
50
|
+
? bytes + " B"
|
|
51
|
+
: value.toFixed(value < 10 ? 1 : 0) + " " + units[i];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatDate(timestamp: number): string {
|
|
55
|
+
const date = new Date(timestamp);
|
|
56
|
+
const now = new Date();
|
|
57
|
+
const diffMs = now.getTime() - date.getTime();
|
|
58
|
+
const diffMins = Math.floor(diffMs / 60_000);
|
|
59
|
+
if (diffMins < 1) return "Just now";
|
|
60
|
+
if (diffMins < 60) return diffMins + "m ago";
|
|
61
|
+
const diffHrs = Math.floor(diffMins / 60);
|
|
62
|
+
if (diffHrs < 24) return diffHrs + "h ago";
|
|
63
|
+
const diffDays = Math.floor(diffHrs / 24);
|
|
64
|
+
if (diffDays < 7) return diffDays + "d ago";
|
|
65
|
+
return date.toLocaleDateString();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function FileIcon({
|
|
69
|
+
contentType,
|
|
70
|
+
size,
|
|
71
|
+
color,
|
|
72
|
+
}: {
|
|
73
|
+
contentType: string;
|
|
74
|
+
size: number;
|
|
75
|
+
color: string;
|
|
76
|
+
}) {
|
|
77
|
+
if (contentType.startsWith("image/"))
|
|
78
|
+
return <FileImage size={size} color={color} />;
|
|
79
|
+
if (contentType.startsWith("video/"))
|
|
80
|
+
return <Film size={size} color={color} />;
|
|
81
|
+
if (contentType.startsWith("audio/"))
|
|
82
|
+
return <Music size={size} color={color} />;
|
|
83
|
+
if (
|
|
84
|
+
contentType === "application/pdf" ||
|
|
85
|
+
contentType.startsWith("text/")
|
|
86
|
+
)
|
|
87
|
+
return <FileText size={size} color={color} />;
|
|
88
|
+
return <File size={size} color={color} />;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function SetupRequired({ message }: { message: string }) {
|
|
92
|
+
return (
|
|
93
|
+
<View style={styles.setupContainer}>
|
|
94
|
+
<Text style={styles.setupTitle}>Setup Required</Text>
|
|
95
|
+
<Text style={styles.setupText}>{message}</Text>
|
|
96
|
+
</View>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function FilesContent({
|
|
101
|
+
db,
|
|
102
|
+
}: {
|
|
103
|
+
db: NonNullable<typeof instantDb>;
|
|
104
|
+
}) {
|
|
105
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
106
|
+
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
107
|
+
|
|
108
|
+
const { isLoading, error, data } = db.useQuery({
|
|
109
|
+
cloudflareObjects: {
|
|
110
|
+
$: {
|
|
111
|
+
order: { createdAt: "desc" },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const files = data?.cloudflareObjects ?? [];
|
|
117
|
+
const totalSize = files.reduce((sum, f) => sum + (f.size ?? 0), 0);
|
|
118
|
+
|
|
119
|
+
const pickAndUpload = useCallback(async () => {
|
|
120
|
+
try {
|
|
121
|
+
const result = await DocumentPicker.getDocumentAsync({
|
|
122
|
+
copyToCacheDirectory: true,
|
|
123
|
+
multiple: false,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (result.canceled || !result.assets?.length) return;
|
|
127
|
+
|
|
128
|
+
const asset = result.assets[0];
|
|
129
|
+
setIsUploading(true);
|
|
130
|
+
|
|
131
|
+
const response = await fetch(asset.uri);
|
|
132
|
+
const blob = await response.blob();
|
|
133
|
+
|
|
134
|
+
await uploadCloudflareObject({
|
|
135
|
+
body: blob,
|
|
136
|
+
contentType: asset.mimeType || "application/octet-stream",
|
|
137
|
+
fileName: asset.name || "untitled",
|
|
138
|
+
});
|
|
139
|
+
} catch (uploadError) {
|
|
140
|
+
console.error("Upload failed:", uploadError);
|
|
141
|
+
Alert.alert(
|
|
142
|
+
"Upload Failed",
|
|
143
|
+
uploadError instanceof Error
|
|
144
|
+
? uploadError.message
|
|
145
|
+
: "Could not upload file.",
|
|
146
|
+
);
|
|
147
|
+
} finally {
|
|
148
|
+
setIsUploading(false);
|
|
149
|
+
}
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
const handleDownload = useCallback(
|
|
153
|
+
async (file: InstantCloudflareObject) => {
|
|
154
|
+
try {
|
|
155
|
+
const workerUrl = process.env.EXPO_PUBLIC_STORAGE_WORKER_URL;
|
|
156
|
+
if (!workerUrl) {
|
|
157
|
+
Alert.alert("Error", "Storage worker URL is not configured.");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const downloadUrl =
|
|
161
|
+
workerUrl + "/uploads/" + file.uploadId + "/content";
|
|
162
|
+
await Linking.openURL(downloadUrl);
|
|
163
|
+
} catch (downloadError) {
|
|
164
|
+
console.error("Download failed:", downloadError);
|
|
165
|
+
Alert.alert("Error", "Could not open file.");
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
[],
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const handleDelete = useCallback(
|
|
172
|
+
async (file: InstantCloudflareObject) => {
|
|
173
|
+
const performDelete = async () => {
|
|
174
|
+
setDeletingId(file.uploadId);
|
|
175
|
+
try {
|
|
176
|
+
await deleteCloudflareObject(file.uploadId);
|
|
177
|
+
} catch (deleteError) {
|
|
178
|
+
console.error("Delete failed:", deleteError);
|
|
179
|
+
Alert.alert(
|
|
180
|
+
"Delete Failed",
|
|
181
|
+
deleteError instanceof Error
|
|
182
|
+
? deleteError.message
|
|
183
|
+
: "Could not delete file.",
|
|
184
|
+
);
|
|
185
|
+
} finally {
|
|
186
|
+
setDeletingId(null);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
if (Platform.OS === "web") {
|
|
191
|
+
const confirmed = window.confirm(
|
|
192
|
+
"Delete " + file.fileName + "?",
|
|
193
|
+
);
|
|
194
|
+
if (confirmed) await performDelete();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
Alert.alert(
|
|
199
|
+
"Delete File",
|
|
200
|
+
"Are you sure you want to delete " + file.fileName + "?",
|
|
201
|
+
[
|
|
202
|
+
{ text: "Cancel", style: "cancel" },
|
|
203
|
+
{
|
|
204
|
+
text: "Delete",
|
|
205
|
+
style: "destructive",
|
|
206
|
+
onPress: performDelete,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
);
|
|
210
|
+
},
|
|
211
|
+
[],
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if (isLoading) {
|
|
215
|
+
return (
|
|
216
|
+
<View style={styles.loadingContainer}>
|
|
217
|
+
<ActivityIndicator size="large" color="#4B5563" />
|
|
218
|
+
<Text style={styles.loadingText}>Loading files...</Text>
|
|
219
|
+
</View>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (error) {
|
|
224
|
+
return (
|
|
225
|
+
<View style={styles.loadingContainer}>
|
|
226
|
+
<Text style={styles.errorText}>Error: {error.message}</Text>
|
|
227
|
+
</View>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<SafeAreaView style={styles.container}>
|
|
233
|
+
<StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
|
|
234
|
+
|
|
235
|
+
<View style={styles.header}>
|
|
236
|
+
<Text style={styles.title}>Files</Text>
|
|
237
|
+
<Text style={styles.subtitle}>
|
|
238
|
+
{files.length} {files.length === 1 ? "file" : "files"}
|
|
239
|
+
{files.length > 0 ? " · " + formatBytes(totalSize) : ""}
|
|
240
|
+
</Text>
|
|
241
|
+
</View>
|
|
242
|
+
|
|
243
|
+
<View style={styles.uploadRow}>
|
|
244
|
+
<Pressable
|
|
245
|
+
style={[
|
|
246
|
+
styles.uploadButton,
|
|
247
|
+
isUploading && styles.uploadButtonDisabled,
|
|
248
|
+
]}
|
|
249
|
+
onPress={pickAndUpload}
|
|
250
|
+
disabled={isUploading}
|
|
251
|
+
>
|
|
252
|
+
{isUploading ? (
|
|
253
|
+
<ActivityIndicator size={18} color="#FFFFFF" />
|
|
254
|
+
) : (
|
|
255
|
+
<Upload size={18} color="#FFFFFF" />
|
|
256
|
+
)}
|
|
257
|
+
<Text style={styles.uploadButtonText}>
|
|
258
|
+
{isUploading ? "Uploading..." : "Upload File"}
|
|
259
|
+
</Text>
|
|
260
|
+
</Pressable>
|
|
261
|
+
</View>
|
|
262
|
+
|
|
263
|
+
<FlatList
|
|
264
|
+
data={files}
|
|
265
|
+
keyExtractor={(item) => item.id}
|
|
266
|
+
contentContainerStyle={styles.listContent}
|
|
267
|
+
ListEmptyComponent={
|
|
268
|
+
<View style={styles.emptyState}>
|
|
269
|
+
<View style={styles.emptyIconWrap}>
|
|
270
|
+
<Plus size={32} color="#9CA3AF" />
|
|
271
|
+
</View>
|
|
272
|
+
<Text style={styles.emptyTitle}>No files yet</Text>
|
|
273
|
+
<Text style={styles.emptySubtitle}>
|
|
274
|
+
Tap "Upload File" to add your first file.
|
|
275
|
+
</Text>
|
|
276
|
+
</View>
|
|
277
|
+
}
|
|
278
|
+
renderItem={({ item }) => {
|
|
279
|
+
const isDeleting = deletingId === item.uploadId;
|
|
280
|
+
return (
|
|
281
|
+
<View style={styles.fileRow}>
|
|
282
|
+
<View style={styles.fileIconWrap}>
|
|
283
|
+
<FileIcon
|
|
284
|
+
contentType={item.contentType}
|
|
285
|
+
size={22}
|
|
286
|
+
color="#6B7280"
|
|
287
|
+
/>
|
|
288
|
+
</View>
|
|
289
|
+
|
|
290
|
+
<View style={styles.fileInfo}>
|
|
291
|
+
<Text
|
|
292
|
+
style={styles.fileName}
|
|
293
|
+
numberOfLines={1}
|
|
294
|
+
ellipsizeMode="middle"
|
|
295
|
+
>
|
|
296
|
+
{item.fileName}
|
|
297
|
+
</Text>
|
|
298
|
+
<Text style={styles.fileMeta}>
|
|
299
|
+
{formatBytes(item.size)} ·{" "}
|
|
300
|
+
{formatDate(item.createdAt)}
|
|
301
|
+
</Text>
|
|
302
|
+
</View>
|
|
303
|
+
|
|
304
|
+
<Pressable
|
|
305
|
+
style={styles.actionButton}
|
|
306
|
+
onPress={() => handleDownload(item)}
|
|
307
|
+
>
|
|
308
|
+
<Download size={18} color="#6B7280" />
|
|
309
|
+
</Pressable>
|
|
310
|
+
|
|
311
|
+
<Pressable
|
|
312
|
+
style={styles.actionButton}
|
|
313
|
+
onPress={() => handleDelete(item)}
|
|
314
|
+
disabled={isDeleting}
|
|
315
|
+
>
|
|
316
|
+
{isDeleting ? (
|
|
317
|
+
<ActivityIndicator size={16} color="#9CA3AF" />
|
|
318
|
+
) : (
|
|
319
|
+
<Trash2 size={18} color="#9CA3AF" />
|
|
320
|
+
)}
|
|
321
|
+
</Pressable>
|
|
322
|
+
</View>
|
|
323
|
+
);
|
|
324
|
+
}}
|
|
325
|
+
/>
|
|
326
|
+
</SafeAreaView>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export default function FilesScreen() {
|
|
331
|
+
if (!instantDb) {
|
|
332
|
+
return (
|
|
333
|
+
<SafeAreaView style={styles.container}>
|
|
334
|
+
<SetupRequired message="Add EXPO_PUBLIC_INSTANT_APP_ID to your .env.local file." />
|
|
335
|
+
</SafeAreaView>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!isCloudflareStorageConfigured()) {
|
|
340
|
+
return (
|
|
341
|
+
<SafeAreaView style={styles.container}>
|
|
342
|
+
<SetupRequired message="Deploy Cloudflare storage and add EXPO_PUBLIC_STORAGE_WORKER_URL to .env.local." />
|
|
343
|
+
</SafeAreaView>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return <FilesContent db={instantDb} />;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const styles = StyleSheet.create({
|
|
351
|
+
container: {
|
|
352
|
+
flex: 1,
|
|
353
|
+
backgroundColor: "#FFFFFF",
|
|
354
|
+
},
|
|
355
|
+
loadingContainer: {
|
|
356
|
+
flex: 1,
|
|
357
|
+
justifyContent: "center",
|
|
358
|
+
alignItems: "center",
|
|
359
|
+
gap: 12,
|
|
360
|
+
paddingHorizontal: 24,
|
|
361
|
+
},
|
|
362
|
+
loadingText: {
|
|
363
|
+
color: "#4B5563",
|
|
364
|
+
fontSize: 16,
|
|
365
|
+
},
|
|
366
|
+
errorText: {
|
|
367
|
+
color: "#DC2626",
|
|
368
|
+
fontSize: 15,
|
|
369
|
+
textAlign: "center",
|
|
370
|
+
},
|
|
371
|
+
header: {
|
|
372
|
+
paddingHorizontal: 16,
|
|
373
|
+
paddingTop: 24,
|
|
374
|
+
paddingBottom: 16,
|
|
375
|
+
},
|
|
376
|
+
title: {
|
|
377
|
+
fontSize: 28,
|
|
378
|
+
fontWeight: "700",
|
|
379
|
+
color: "#1F2937",
|
|
380
|
+
marginBottom: 4,
|
|
381
|
+
},
|
|
382
|
+
subtitle: {
|
|
383
|
+
fontSize: 14,
|
|
384
|
+
color: "#6B7280",
|
|
385
|
+
fontWeight: "500",
|
|
386
|
+
},
|
|
387
|
+
uploadRow: {
|
|
388
|
+
paddingHorizontal: 16,
|
|
389
|
+
paddingBottom: 14,
|
|
390
|
+
},
|
|
391
|
+
uploadButton: {
|
|
392
|
+
flexDirection: "row",
|
|
393
|
+
alignItems: "center",
|
|
394
|
+
justifyContent: "center",
|
|
395
|
+
gap: 8,
|
|
396
|
+
backgroundColor: "#1F2937",
|
|
397
|
+
borderRadius: 12,
|
|
398
|
+
paddingVertical: 14,
|
|
399
|
+
},
|
|
400
|
+
uploadButtonDisabled: {
|
|
401
|
+
opacity: 0.6,
|
|
402
|
+
},
|
|
403
|
+
uploadButtonText: {
|
|
404
|
+
color: "#FFFFFF",
|
|
405
|
+
fontSize: 16,
|
|
406
|
+
fontWeight: "600",
|
|
407
|
+
},
|
|
408
|
+
listContent: {
|
|
409
|
+
paddingHorizontal: 16,
|
|
410
|
+
paddingBottom: 96,
|
|
411
|
+
flexGrow: 1,
|
|
412
|
+
},
|
|
413
|
+
fileRow: {
|
|
414
|
+
flexDirection: "row",
|
|
415
|
+
alignItems: "center",
|
|
416
|
+
minHeight: 64,
|
|
417
|
+
borderRadius: 12,
|
|
418
|
+
borderWidth: 1,
|
|
419
|
+
borderColor: "#E5E7EB",
|
|
420
|
+
marginBottom: 8,
|
|
421
|
+
paddingHorizontal: 12,
|
|
422
|
+
paddingVertical: 10,
|
|
423
|
+
backgroundColor: "#FFFFFF",
|
|
424
|
+
},
|
|
425
|
+
fileIconWrap: {
|
|
426
|
+
width: 40,
|
|
427
|
+
height: 40,
|
|
428
|
+
borderRadius: 10,
|
|
429
|
+
backgroundColor: "#F3F4F6",
|
|
430
|
+
alignItems: "center",
|
|
431
|
+
justifyContent: "center",
|
|
432
|
+
marginRight: 12,
|
|
433
|
+
},
|
|
434
|
+
fileInfo: {
|
|
435
|
+
flex: 1,
|
|
436
|
+
marginRight: 4,
|
|
437
|
+
},
|
|
438
|
+
fileName: {
|
|
439
|
+
fontSize: 15,
|
|
440
|
+
fontWeight: "500",
|
|
441
|
+
color: "#1F2937",
|
|
442
|
+
marginBottom: 2,
|
|
443
|
+
},
|
|
444
|
+
fileMeta: {
|
|
445
|
+
fontSize: 13,
|
|
446
|
+
color: "#9CA3AF",
|
|
447
|
+
},
|
|
448
|
+
actionButton: {
|
|
449
|
+
width: 38,
|
|
450
|
+
height: 38,
|
|
451
|
+
borderRadius: 10,
|
|
452
|
+
alignItems: "center",
|
|
453
|
+
justifyContent: "center",
|
|
454
|
+
},
|
|
455
|
+
setupContainer: {
|
|
456
|
+
flex: 1,
|
|
457
|
+
justifyContent: "center",
|
|
458
|
+
paddingHorizontal: 24,
|
|
459
|
+
},
|
|
460
|
+
setupTitle: {
|
|
461
|
+
fontSize: 24,
|
|
462
|
+
fontWeight: "700",
|
|
463
|
+
color: "#111827",
|
|
464
|
+
marginBottom: 12,
|
|
465
|
+
textAlign: "center",
|
|
466
|
+
},
|
|
467
|
+
setupText: {
|
|
468
|
+
fontSize: 15,
|
|
469
|
+
color: "#4B5563",
|
|
470
|
+
textAlign: "center",
|
|
471
|
+
lineHeight: 22,
|
|
472
|
+
},
|
|
473
|
+
emptyState: {
|
|
474
|
+
marginTop: 64,
|
|
475
|
+
alignItems: "center",
|
|
476
|
+
},
|
|
477
|
+
emptyIconWrap: {
|
|
478
|
+
width: 64,
|
|
479
|
+
height: 64,
|
|
480
|
+
borderRadius: 32,
|
|
481
|
+
backgroundColor: "#F3F4F6",
|
|
482
|
+
alignItems: "center",
|
|
483
|
+
justifyContent: "center",
|
|
484
|
+
marginBottom: 16,
|
|
485
|
+
},
|
|
486
|
+
emptyTitle: {
|
|
487
|
+
fontSize: 18,
|
|
488
|
+
fontWeight: "600",
|
|
489
|
+
color: "#1F2937",
|
|
490
|
+
},
|
|
491
|
+
emptySubtitle: {
|
|
492
|
+
marginTop: 8,
|
|
493
|
+
color: "#6B7280",
|
|
494
|
+
fontSize: 14,
|
|
495
|
+
textAlign: "center",
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
`;
|
|
499
|
+
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.agentsMdTemplate = agentsMdTemplate;
|
|
4
|
+
function agentsMdTemplate(projectName, backend, instantAuthMode = "clerk") {
|
|
5
|
+
if (backend === "instantdb") {
|
|
6
|
+
if (instantAuthMode === "magic-code-cloudflare") {
|
|
7
|
+
return magicCodeCloudflareAgentsMd(projectName);
|
|
8
|
+
}
|
|
9
|
+
if (instantAuthMode === "magic-code") {
|
|
10
|
+
return magicCodeAgentsMd(projectName);
|
|
11
|
+
}
|
|
12
|
+
return instantClerkAgentsMd(projectName);
|
|
13
|
+
}
|
|
14
|
+
return convexClerkAgentsMd(projectName);
|
|
15
|
+
}
|
|
16
|
+
function convexClerkAgentsMd(projectName) {
|
|
17
|
+
return `# AGENTS.md — ${projectName}
|
|
18
|
+
|
|
19
|
+
> AI agent context file. This project was scaffolded with \`create-100x-mobile\`.
|
|
20
|
+
|
|
21
|
+
## Tech Stack
|
|
22
|
+
|
|
23
|
+
| Layer | Technology |
|
|
24
|
+
|-------|-----------|
|
|
25
|
+
| Framework | Expo (React Native) with Expo Router (file-based routing) |
|
|
26
|
+
| Backend | Convex — realtime serverless backend |
|
|
27
|
+
| Auth | Clerk — OAuth (Google, Apple), session management |
|
|
28
|
+
| Language | TypeScript (strict) |
|
|
29
|
+
| Styling | React Native StyleSheet (no Tailwind) |
|
|
30
|
+
|
|
31
|
+
## Project Structure
|
|
32
|
+
|
|
33
|
+
\`\`\`
|
|
34
|
+
├── app/
|
|
35
|
+
│ ├── _layout.tsx # Root layout with Clerk + Convex providers
|
|
36
|
+
│ ├── +not-found.tsx # 404 screen
|
|
37
|
+
│ ├── (auth)/
|
|
38
|
+
│ │ ├── _layout.tsx # Auth guard layout
|
|
39
|
+
│ │ └── sign-in.tsx # Sign-in screen (Clerk OAuth)
|
|
40
|
+
│ ├── (tabs)/
|
|
41
|
+
│ │ ├── _layout.tsx # Tab navigator
|
|
42
|
+
│ │ ├── index.tsx # Todos screen
|
|
43
|
+
│ │ └── settings.tsx # Settings screen
|
|
44
|
+
│ └── providers/
|
|
45
|
+
│ └── AuthProvider.tsx # Clerk + Convex auth bridge
|
|
46
|
+
├── components/ # Reusable UI components
|
|
47
|
+
├── convex/
|
|
48
|
+
│ ├── schema.ts # Database schema (todos, users)
|
|
49
|
+
│ ├── todos.ts # Todo mutations and queries
|
|
50
|
+
│ ├── users.ts # User functions
|
|
51
|
+
│ ├── auth.config.ts # Clerk JWT issuer config
|
|
52
|
+
│ ├── http.ts # HTTP routes
|
|
53
|
+
│ └── tsconfig.json # Convex-specific tsconfig
|
|
54
|
+
├── hooks/
|
|
55
|
+
│ └── useFrameworkReady.ts # Expo framework ready hook
|
|
56
|
+
└── assets/ # Images and static assets
|
|
57
|
+
\`\`\`
|
|
58
|
+
|
|
59
|
+
## Environment Variables
|
|
60
|
+
|
|
61
|
+
File: \`.env.local\` (never commit this file)
|
|
62
|
+
|
|
63
|
+
| Variable | Purpose | Source |
|
|
64
|
+
|----------|---------|--------|
|
|
65
|
+
| \`EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY\` | Clerk public key | Clerk Dashboard → API Keys |
|
|
66
|
+
| \`CLERK_JWT_ISSUER_DOMAIN\` | Clerk JWT issuer domain | Auto-derived from publishable key |
|
|
67
|
+
| \`EXPO_PUBLIC_CONVEX_URL\` | Convex deployment URL | \`bunx convex dev\` output |
|
|
68
|
+
|
|
69
|
+
## Commands
|
|
70
|
+
|
|
71
|
+
| Command | Purpose |
|
|
72
|
+
|---------|---------|
|
|
73
|
+
| \`bun install\` | Install dependencies |
|
|
74
|
+
| \`bunx convex dev\` | Start Convex dev server (run in separate terminal) |
|
|
75
|
+
| \`bunx expo start\` | Start Expo dev server |
|
|
76
|
+
| \`bun run lint\` | Run ESLint |
|
|
77
|
+
| \`bun run format\` | Format with Prettier |
|
|
78
|
+
| \`bunx convex deploy\` | Deploy Convex to production |
|
|
79
|
+
| \`bunx eas build --profile development --platform ios\` | Dev build for iOS |
|
|
80
|
+
|
|
81
|
+
## Key Patterns
|
|
82
|
+
|
|
83
|
+
### Adding a new database table
|
|
84
|
+
|
|
85
|
+
1. Add the entity to \`convex/schema.ts\`
|
|
86
|
+
2. Create queries/mutations in \`convex/<tableName>.ts\`
|
|
87
|
+
3. Use \`useQuery()\` and \`useMutation()\` from \`convex/react\` in components
|
|
88
|
+
|
|
89
|
+
### Auth flow
|
|
90
|
+
|
|
91
|
+
1. Clerk handles OAuth (Google/Apple) via \`expo-auth-session\`
|
|
92
|
+
2. Clerk session token is forwarded to Convex via \`ConvexProviderWithClerk\`
|
|
93
|
+
3. Convex validates the JWT using the issuer domain in \`auth.config.ts\`
|
|
94
|
+
4. Server functions access the user via \`ctx.auth.getUserIdentity()\`
|
|
95
|
+
|
|
96
|
+
### Navigation
|
|
97
|
+
|
|
98
|
+
- File-based routing via Expo Router
|
|
99
|
+
- \`(auth)\` group for unauthenticated screens
|
|
100
|
+
- \`(tabs)\` group for authenticated tab screens
|
|
101
|
+
- Auth guard in \`(tabs)/_layout.tsx\` redirects to sign-in if not authenticated
|
|
102
|
+
|
|
103
|
+
## Constraints
|
|
104
|
+
|
|
105
|
+
- Do NOT commit \`.env.local\` — it contains secrets
|
|
106
|
+
- Do NOT modify \`convex/_generated/\` — these files are auto-generated
|
|
107
|
+
- Convex functions must be deterministic; no \`Date.now()\` or \`Math.random()\` in queries
|
|
108
|
+
- All Convex mutations must use \`ctx.db\` methods, not direct database access
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
function instantClerkAgentsMd(projectName) {
|
|
112
|
+
return `# AGENTS.md — ${projectName}
|
|
113
|
+
|
|
114
|
+
> AI agent context file. This project was scaffolded with \`create-100x-mobile\`.
|
|
115
|
+
|
|
116
|
+
## Tech Stack
|
|
117
|
+
|
|
118
|
+
| Layer | Technology |
|
|
119
|
+
|-------|-----------|
|
|
120
|
+
| Framework | Expo (React Native) with Expo Router (file-based routing) |
|
|
121
|
+
| Backend | InstantDB — client-first realtime database |
|
|
122
|
+
| Auth | Clerk — OAuth, then bridged to InstantDB via client name |
|
|
123
|
+
| Language | TypeScript (strict) |
|
|
124
|
+
| Styling | React Native StyleSheet (no Tailwind) |
|
|
125
|
+
|
|
126
|
+
## Project Structure
|
|
127
|
+
|
|
128
|
+
\`\`\`
|
|
129
|
+
├── app/
|
|
130
|
+
│ ├── _layout.tsx # Root layout with Clerk provider
|
|
131
|
+
│ ├── +not-found.tsx # 404 screen
|
|
132
|
+
│ ├── (auth)/
|
|
133
|
+
│ │ ├── _layout.tsx # Auth guard layout
|
|
134
|
+
│ │ └── sign-in.tsx # Sign-in screen
|
|
135
|
+
│ ├── (tabs)/
|
|
136
|
+
│ │ ├── _layout.tsx # Tab navigator
|
|
137
|
+
│ │ ├── index.tsx # Todos screen
|
|
138
|
+
│ │ └── settings.tsx # Settings screen
|
|
139
|
+
│ └── providers/
|
|
140
|
+
│ └── AuthProvider.tsx # Clerk + InstantDB bridge
|
|
141
|
+
├── lib/
|
|
142
|
+
│ └── instant.ts # InstantDB schema and initialization
|
|
143
|
+
├── hooks/
|
|
144
|
+
│ └── useFrameworkReady.ts
|
|
145
|
+
└── assets/
|
|
146
|
+
\`\`\`
|
|
147
|
+
|
|
148
|
+
## Environment Variables
|
|
149
|
+
|
|
150
|
+
File: \`.env.local\` (never commit this file)
|
|
151
|
+
|
|
152
|
+
| Variable | Purpose | Source |
|
|
153
|
+
|----------|---------|--------|
|
|
154
|
+
| \`EXPO_PUBLIC_INSTANT_APP_ID\` | InstantDB app identifier | \`npx instant-cli init-without-files\` |
|
|
155
|
+
| \`EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY\` | Clerk public key | Clerk Dashboard → API Keys |
|
|
156
|
+
| \`EXPO_PUBLIC_INSTANT_CLERK_CLIENT_NAME\` | Clerk client name in Instant auth | Instant Dashboard → Auth tab |
|
|
157
|
+
|
|
158
|
+
## Commands
|
|
159
|
+
|
|
160
|
+
| Command | Purpose |
|
|
161
|
+
|---------|---------|
|
|
162
|
+
| \`bun install\` | Install dependencies |
|
|
163
|
+
| \`bunx expo start\` | Start Expo dev server |
|
|
164
|
+
| \`bun run lint\` | Run ESLint |
|
|
165
|
+
| \`bun run format\` | Format with Prettier |
|
|
166
|
+
|
|
167
|
+
## Key Patterns
|
|
168
|
+
|
|
169
|
+
### Adding a new entity
|
|
170
|
+
|
|
171
|
+
1. Add the entity to the schema in \`lib/instant.ts\`:
|
|
172
|
+
\`\`\`ts
|
|
173
|
+
myEntity: i.entity({ name: i.string(), createdAt: i.number().indexed() })
|
|
174
|
+
\`\`\`
|
|
175
|
+
2. Export the type: \`export type MyEntity = InstaQLEntity<typeof schema, "myEntity">\`
|
|
176
|
+
3. Query in components: \`db.useQuery({ myEntity: {} })\`
|
|
177
|
+
4. Write: \`db.transact(db.tx.myEntity[id()].create({ ... }))\`
|
|
178
|
+
|
|
179
|
+
### Auth flow
|
|
180
|
+
|
|
181
|
+
1. Clerk authenticates the user (OAuth)
|
|
182
|
+
2. Clerk token is passed to InstantDB as a third-party auth client
|
|
183
|
+
3. InstantDB verifies via the registered Clerk client name
|
|
184
|
+
4. Clerk session customization must include \`email\` and \`email_verified\` claims
|
|
185
|
+
|
|
186
|
+
## Constraints
|
|
187
|
+
|
|
188
|
+
- Do NOT commit \`.env.local\`
|
|
189
|
+
- InstantDB schema is defined client-side in \`lib/instant.ts\` — keep it in sync with Instant Dashboard
|
|
190
|
+
- \`id()\` from \`@instantdb/react-native\` generates IDs; never use custom string IDs
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
193
|
+
function magicCodeAgentsMd(projectName) {
|
|
194
|
+
return `# AGENTS.md — ${projectName}
|
|
195
|
+
|
|
196
|
+
> AI agent context file. This project was scaffolded with \`create-100x-mobile\`.
|
|
197
|
+
|
|
198
|
+
## Tech Stack
|
|
199
|
+
|
|
200
|
+
| Layer | Technology |
|
|
201
|
+
|-------|-----------|
|
|
202
|
+
| Framework | Expo (React Native) with Expo Router (file-based routing) |
|
|
203
|
+
| Backend | InstantDB — client-first realtime database with built-in auth |
|
|
204
|
+
| Auth | InstantDB magic code — email-based one-time code authentication |
|
|
205
|
+
| Language | TypeScript (strict) |
|
|
206
|
+
| Styling | React Native StyleSheet (no Tailwind) |
|
|
207
|
+
|
|
208
|
+
## Project Structure
|
|
209
|
+
|
|
210
|
+
\`\`\`
|
|
211
|
+
├── app/
|
|
212
|
+
│ ├── _layout.tsx # Root layout
|
|
213
|
+
│ ├── +not-found.tsx # 404 screen
|
|
214
|
+
│ ├── (auth)/
|
|
215
|
+
│ │ ├── _layout.tsx # Auth guard layout
|
|
216
|
+
│ │ └── sign-in.tsx # Magic code sign-in (email + code input)
|
|
217
|
+
│ ├── (tabs)/
|
|
218
|
+
│ │ ├── _layout.tsx # Tab navigator (Todos, Settings)
|
|
219
|
+
│ │ ├── index.tsx # Todos screen
|
|
220
|
+
│ │ └── settings.tsx # Settings screen
|
|
221
|
+
├── lib/
|
|
222
|
+
│ └── instant.ts # InstantDB schema and initialization
|
|
223
|
+
├── hooks/
|
|
224
|
+
│ └── useFrameworkReady.ts
|
|
225
|
+
└── assets/
|
|
226
|
+
\`\`\`
|
|
227
|
+
|
|
228
|
+
## Environment Variables
|
|
229
|
+
|
|
230
|
+
File: \`.env.local\` (never commit this file)
|
|
231
|
+
|
|
232
|
+
| Variable | Purpose | Source |
|
|
233
|
+
|----------|---------|--------|
|
|
234
|
+
| \`EXPO_PUBLIC_INSTANT_APP_ID\` | InstantDB app identifier | \`npx instant-cli init-without-files\` |
|
|
235
|
+
|
|
236
|
+
## Commands
|
|
237
|
+
|
|
238
|
+
| Command | Purpose |
|
|
239
|
+
|---------|---------|
|
|
240
|
+
| \`bun install\` | Install dependencies |
|
|
241
|
+
| \`bunx expo start\` | Start Expo dev server |
|
|
242
|
+
| \`bun run lint\` | Run ESLint |
|
|
243
|
+
| \`bun run format\` | Format with Prettier |
|
|
244
|
+
|
|
245
|
+
## Key Patterns
|
|
246
|
+
|
|
247
|
+
### Adding a new entity
|
|
248
|
+
|
|
249
|
+
1. Add to schema in \`lib/instant.ts\`
|
|
250
|
+
2. Export the type
|
|
251
|
+
3. Query: \`db.useQuery({ entityName: {} })\`
|
|
252
|
+
4. Write: \`db.transact(db.tx.entityName[id()].create({ ... }))\`
|
|
253
|
+
|
|
254
|
+
### Auth flow
|
|
255
|
+
|
|
256
|
+
1. User enters email → \`db.auth.sendMagicCode({ email })\`
|
|
257
|
+
2. User receives code via email
|
|
258
|
+
3. User enters code → \`db.auth.signInWithMagicCode({ email, code })\`
|
|
259
|
+
4. Session persists via \`@react-native-async-storage/async-storage\`
|
|
260
|
+
5. Check auth: \`db.useAuth()\` returns \`{ isLoading, user, error }\`
|
|
261
|
+
6. Sign out: \`db.auth.signOut()\`
|
|
262
|
+
|
|
263
|
+
## Constraints
|
|
264
|
+
|
|
265
|
+
- Do NOT commit \`.env.local\`
|
|
266
|
+
- No external auth provider needed — InstantDB handles everything
|
|
267
|
+
- \`id()\` from \`@instantdb/react-native\` generates IDs
|
|
268
|
+
`;
|
|
269
|
+
}
|
|
270
|
+
function magicCodeCloudflareAgentsMd(projectName) {
|
|
271
|
+
return `# AGENTS.md — ${projectName}
|
|
272
|
+
|
|
273
|
+
> AI agent context file. This project was scaffolded with \`create-100x-mobile\`.
|
|
274
|
+
|
|
275
|
+
## Tech Stack
|
|
276
|
+
|
|
277
|
+
| Layer | Technology |
|
|
278
|
+
|-------|-----------|
|
|
279
|
+
| Framework | Expo (React Native) with Expo Router (file-based routing) |
|
|
280
|
+
| Backend | InstantDB — client-first realtime database with built-in auth |
|
|
281
|
+
| Auth | InstantDB magic code — email-based one-time code authentication |
|
|
282
|
+
| Storage | Cloudflare R2 (files) + D1 (metadata) via a Worker API |
|
|
283
|
+
| Infrastructure | Alchemy — TypeScript IaC for Cloudflare resources |
|
|
284
|
+
| Language | TypeScript (strict) |
|
|
285
|
+
| Styling | React Native StyleSheet (no Tailwind) |
|
|
286
|
+
|
|
287
|
+
## Architecture
|
|
288
|
+
|
|
289
|
+
\`\`\`
|
|
290
|
+
Expo App ──realtime──▸ InstantDB (auth, todos, file metadata)
|
|
291
|
+
│
|
|
292
|
+
└── upload/download/delete ──▸ Cloudflare Worker
|
|
293
|
+
├── R2 Bucket (file bytes)
|
|
294
|
+
├── D1 Database (upload metadata)
|
|
295
|
+
└── syncs back to InstantDB
|
|
296
|
+
\`\`\`
|
|
297
|
+
|
|
298
|
+
## Project Structure
|
|
299
|
+
|
|
300
|
+
\`\`\`
|
|
301
|
+
├── app/
|
|
302
|
+
│ ├── _layout.tsx # Root layout
|
|
303
|
+
│ ├── +not-found.tsx
|
|
304
|
+
│ ├── (auth)/
|
|
305
|
+
│ │ ├── _layout.tsx # Auth guard layout
|
|
306
|
+
│ │ └── sign-in.tsx # Magic code sign-in
|
|
307
|
+
│ ├── (tabs)/
|
|
308
|
+
│ │ ├── _layout.tsx # Tab navigator (Todos, Files, Settings)
|
|
309
|
+
│ │ ├── index.tsx # Todos screen
|
|
310
|
+
│ │ ├── files.tsx # File manager (upload, download, delete)
|
|
311
|
+
│ │ └── settings.tsx # Settings screen
|
|
312
|
+
├── lib/
|
|
313
|
+
│ ├── instant.ts # InstantDB schema (todos + cloudflareObjects)
|
|
314
|
+
│ └── cloudflareStorage.ts # Upload, download, delete API client
|
|
315
|
+
├── worker/
|
|
316
|
+
│ ├── src/worker.ts # Cloudflare Worker entry point
|
|
317
|
+
│ ├── tsconfig.json
|
|
318
|
+
│ └── migrations/
|
|
319
|
+
│ └── 0001_uploads.sql # D1 uploads table
|
|
320
|
+
├── alchemy.run.ts # Alchemy IaC (R2 bucket, D1 database, Worker)
|
|
321
|
+
├── hooks/
|
|
322
|
+
│ └── useFrameworkReady.ts
|
|
323
|
+
└── assets/
|
|
324
|
+
\`\`\`
|
|
325
|
+
|
|
326
|
+
## Environment Variables
|
|
327
|
+
|
|
328
|
+
File: \`.env.local\` (never commit this file)
|
|
329
|
+
|
|
330
|
+
| Variable | Purpose | Source |
|
|
331
|
+
|----------|---------|--------|
|
|
332
|
+
| \`EXPO_PUBLIC_INSTANT_APP_ID\` | InstantDB app ID | \`npx instant-cli init-without-files\` |
|
|
333
|
+
| \`EXPO_PUBLIC_STORAGE_WORKER_URL\` | Deployed Worker URL | Output of \`bun run cloudflare:deploy\` |
|
|
334
|
+
| \`INSTANT_APP_ID\` | Same as above, used by Alchemy for Worker bindings | Same value |
|
|
335
|
+
| \`INSTANT_ADMIN_TOKEN\` | InstantDB admin token for Worker metadata sync | Instant Dashboard → Admin Tokens |
|
|
336
|
+
| \`ALLOWED_ORIGINS\` | CORS origins for the Worker | Comma-separated URLs |
|
|
337
|
+
| \`MAX_UPLOAD_BYTES\` | Max single file size (default 25 MB) | Integer |
|
|
338
|
+
| \`USER_STORAGE_LIMIT_BYTES\` | Per-user storage cap (default 500 MB) | Integer |
|
|
339
|
+
| \`DAILY_UPLOAD_LIMIT\` | Max uploads per user per day (default 100) | Integer |
|
|
340
|
+
| \`ALLOWED_CONTENT_TYPES\` | Allowed MIME types | Comma-separated |
|
|
341
|
+
|
|
342
|
+
## Commands
|
|
343
|
+
|
|
344
|
+
| Command | Purpose |
|
|
345
|
+
|---------|---------|
|
|
346
|
+
| \`bun install\` | Install dependencies |
|
|
347
|
+
| \`bunx expo start\` | Start Expo dev server |
|
|
348
|
+
| \`bun run lint\` | Run ESLint |
|
|
349
|
+
| \`bun run format\` | Format with Prettier |
|
|
350
|
+
| \`bun run cloudflare:configure\` | Configure Alchemy credentials |
|
|
351
|
+
| \`bun run cloudflare:login\` | Login to Cloudflare via Alchemy |
|
|
352
|
+
| \`bun run cloudflare:deploy\` | Deploy Worker + R2 + D1 |
|
|
353
|
+
| \`bun run cloudflare:dev\` | Run Worker locally |
|
|
354
|
+
| \`bun run cloudflare:destroy\` | Tear down Cloudflare resources |
|
|
355
|
+
| \`bun run typecheck:worker\` | Type-check Worker code |
|
|
356
|
+
|
|
357
|
+
## Key Patterns
|
|
358
|
+
|
|
359
|
+
### Adding a new InstantDB entity
|
|
360
|
+
|
|
361
|
+
1. Add to \`lib/instant.ts\` schema
|
|
362
|
+
2. Export the type
|
|
363
|
+
3. Query: \`db.useQuery({ entityName: {} })\`
|
|
364
|
+
4. Write: \`db.transact(db.tx.entityName[id()].create({ ... }))\`
|
|
365
|
+
|
|
366
|
+
### File upload flow
|
|
367
|
+
|
|
368
|
+
1. User picks a file with \`expo-document-picker\`
|
|
369
|
+
2. App calls \`uploadCloudflareObject({ body, contentType, fileName })\` from \`lib/cloudflareStorage.ts\`
|
|
370
|
+
3. Client sends the file to \`POST /uploads\` on the Worker with the InstantDB refresh token
|
|
371
|
+
4. Worker verifies token with \`@instantdb/admin\`, stores file in R2, records in D1
|
|
372
|
+
5. Worker syncs metadata to InstantDB \`cloudflareObjects\` entity via admin SDK
|
|
373
|
+
6. App sees the new file in realtime via \`db.useQuery({ cloudflareObjects: {} })\`
|
|
374
|
+
|
|
375
|
+
### File delete flow
|
|
376
|
+
|
|
377
|
+
1. App calls \`deleteCloudflareObject(uploadId)\`
|
|
378
|
+
2. Worker verifies ownership, deletes from R2, D1, and InstantDB
|
|
379
|
+
|
|
380
|
+
### Worker API routes
|
|
381
|
+
|
|
382
|
+
| Method | Path | Purpose |
|
|
383
|
+
|--------|------|---------|
|
|
384
|
+
| \`POST\` | \`/uploads\` | Upload a file |
|
|
385
|
+
| \`GET\` | \`/uploads/:id\` | Get upload metadata |
|
|
386
|
+
| \`GET\` | \`/uploads/:id/content\` | Download file bytes |
|
|
387
|
+
| \`DELETE\` | \`/uploads/:id\` | Delete file |
|
|
388
|
+
| \`GET\` | \`/health\` | Health check |
|
|
389
|
+
|
|
390
|
+
### Auth flow
|
|
391
|
+
|
|
392
|
+
1. User enters email → \`db.auth.sendMagicCode({ email })\`
|
|
393
|
+
2. Code arrives by email
|
|
394
|
+
3. User enters code → \`db.auth.signInWithMagicCode({ email, code })\`
|
|
395
|
+
4. Refresh token is used as Bearer token for Worker requests
|
|
396
|
+
|
|
397
|
+
## Constraints
|
|
398
|
+
|
|
399
|
+
- Do NOT commit \`.env.local\` — contains admin tokens
|
|
400
|
+
- Do NOT expose the R2 bucket publicly — all access goes through the Worker
|
|
401
|
+
- Worker validates ownership before reads, downloads, and deletes
|
|
402
|
+
- The \`cloudflareObjects\` entity in InstantDB is written by the Worker only (via admin token)
|
|
403
|
+
- When adding new Worker routes, add them to \`routeRequest()\` in \`worker/src/worker.ts\`
|
|
404
|
+
- After changing Worker code, redeploy with \`bun run cloudflare:deploy\`
|
|
405
|
+
`;
|
|
406
|
+
}
|
|
@@ -13,7 +13,10 @@ function packageJsonTemplate(appName, expoVersion = "latest", backend = "convex"
|
|
|
13
13
|
? isMagicCodeInstantAuthMode(instantAuthMode)
|
|
14
14
|
? {
|
|
15
15
|
...(isCloudflareInstantAuthMode(instantAuthMode)
|
|
16
|
-
? {
|
|
16
|
+
? {
|
|
17
|
+
"@instantdb/admin": "^1.0.15",
|
|
18
|
+
"expo-document-picker": "~12.2.2",
|
|
19
|
+
}
|
|
17
20
|
: {}),
|
|
18
21
|
"@instantdb/react-native": "^0.20.0",
|
|
19
22
|
"@react-native-async-storage/async-storage": "^2.2.0",
|