@inglorious/create-app 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,7 +31,7 @@ For the JavaScript and TypeScript Vite templates, the CLI then asks about the ap
31
31
  | Add push notifications? | `--push` / `--no-push` | Firebase Cloud Messaging, a permission prompt and the Supabase side of sending; turns on PWA support |
32
32
  | Enable PWA support? | `--pwa` / `--no-pwa` | `vite-plugin-pwa`, a manifest, icons, and a service worker that keeps installed apps on the latest version |
33
33
 
34
- Every template except `minimal` then asks whether to add agent skills (`--skills` / `--no-skills`). Saying yes runs the [skills CLI](https://skills.sh) in the new project. It asks which coding agents to install for, and installs the [Inglorious Forge skills](https://github.com/IngloriousCoderz/forge-skills) that match your choices: `forge`, `forge-best-practices`, `store`, `web` and `web-best-practices` always, plus `ssx`, `vite-plugin-jsx` or `vite-plugin-vue`, `ui` and `ui-best-practices`, `i18n`, `firebase` and `supabase` when the project uses them. If the install fails, the CLI prints the command to retry it.
34
+ Every template except `minimal` then asks whether to add agent skills (`--skills` / `--no-skills`). Saying yes runs the [skills CLI](https://skills.sh) in the new project. It asks which coding agents to install for, and installs the [Inglorious Forge skills](https://github.com/IngloriousCoderz/forge-skills) that match your choices: `forge`, `forge-best-practices`, `app-recipe`, `store`, `web` and `web-best-practices` always, plus `ssx`, `vite-plugin-jsx` or `vite-plugin-vue`, `ui` and `ui-best-practices`, `i18n`, `firebase` and `supabase` when the project uses them. If the install fails, the CLI prints the command to retry it.
35
35
 
36
36
  The app is generated in the syntax you chose: lit-html, JSX or Vue. Every question answered on the command line is skipped, so a project can be created without any prompt:
37
37
 
@@ -99,7 +99,7 @@ For Android and iOS builds, install Android Studio with the Android SDK and a Ja
99
99
 
100
100
  Every Vite app is versioned with [Changesets](https://github.com/changesets/changesets): describe each change with `pnpm changeset`, and `pnpm release:version` raises the version in `package.json` and writes `CHANGELOG.md`. The app reads that version as `import.meta.env.VITE_APP_VERSION`.
101
101
 
102
- Mobile apps also get `pnpm mobile:release`, which copies the version into the iOS and Android projects, raises the build number they share (every store upload needs a new one), builds with the values in `.env` rather than `.env.local`, and syncs. The generated README describes the whole release, upload included.
102
+ Mobile apps also get `pnpm mobile:release`, which copies the version into the iOS and Android projects, raises the build number they share (every store upload needs a new one), builds with the values in `.env` rather than `.env.local`, and syncs. The generated README describes the whole release, upload included. With `--push` too, an `app_versions` table in Supabase holds each platform's minimum build: an older install shows nothing but an update page, so raising it makes a release mandatory.
103
103
 
104
104
  ## License
105
105
 
package/bin/index.js CHANGED
@@ -20,6 +20,7 @@ import { SYNTAXES, TEMPLATES } from "./lib/features.js"
20
20
  import { copyDir } from "./lib/file.js"
21
21
  import { addFirebaseSupport } from "./lib/firebase.js"
22
22
  import { addMobileSupport } from "./lib/mobile.js"
23
+ import { addNativeUpdateSupport } from "./lib/native-update.js"
23
24
  import { addPwaSupport } from "./lib/pwa.js"
24
25
  import { askAppFeatures } from "./lib/questions.js"
25
26
  import { addReleaseSupport } from "./lib/release.js"
@@ -207,6 +208,10 @@ async function main() {
207
208
  addFirebaseSupport(targetDir, pkg, { ...features, templatesRoot })
208
209
  }
209
210
 
211
+ if (features.mobile && features.push) {
212
+ addNativeUpdateSupport(targetDir, templatesRoot)
213
+ }
214
+
210
215
  for (const depType of ["dependencies", "devDependencies"]) {
211
216
  if (pkg[depType]) {
212
217
  for (const [name, version] of Object.entries(pkg[depType])) {
package/bin/lib/app.js CHANGED
@@ -6,14 +6,21 @@ import * as jsxComponents from "./app-jsx.js"
6
6
  import * as litComponents from "./app-lit.js"
7
7
  import * as vueComponents from "./app-vue.js"
8
8
  import {
9
+ CAPACITOR_APP_VERSION,
9
10
  CAPACITOR_FIREBASE_VERSION,
10
11
  CAPACITOR_VERSION,
11
12
  FIREBASE_VERSION,
12
13
  INDENTATION,
13
14
  SUPABASE_VERSION,
14
15
  } from "./constants.js"
15
- import { ICON_PACKS, LANGUAGES, PROMPT_TEXTS } from "./features.js"
16
+ import {
17
+ ICON_PACKS,
18
+ LANGUAGES,
19
+ NATIVE_UPDATE_TEXTS,
20
+ PROMPT_TEXTS,
21
+ } from "./features.js"
16
22
  import { importBlock } from "./imports.js"
23
+ import { nativeUpdateFiles } from "./native-update.js"
17
24
  import { envExample, promptFiles } from "./prompts.js"
18
25
  import {
19
26
  appModel,
@@ -33,7 +40,8 @@ const COMPONENTS = {
33
40
  /**
34
41
  * Writes the source of an app with the features chosen: Inglorious UI,
35
42
  * routing, page transitions, icon packs, internationalization, a mobile
36
- * layout, analytics and push prompts, in lit-html, JSX or Vue. It replaces the template's hello world,
43
+ * layout, analytics and push prompts, and the page that blocks outdated native
44
+ * builds, in lit-html, JSX or Vue. It replaces the template's hello world,
37
45
  * keeping its message type as the example on the home page. The files come
38
46
  * out as Prettier and the template's ESLint config would leave them.
39
47
  *
@@ -60,6 +68,7 @@ export function addAppFeatures(targetDir, pkg, options) {
60
68
  app.links = app.routing && !app.tabs
61
69
  app.isVue = app.renderer === "vue"
62
70
  app.hasPrompts = app.analytics || app.push
71
+ app.nativeUpdate = app.mobile && app.push
63
72
  app.layout = layout(app)
64
73
 
65
74
  const components = COMPONENTS[app.renderer]
@@ -93,6 +102,7 @@ export function addAppFeatures(targetDir, pkg, options) {
93
102
  Object.assign(files, promptFiles(app))
94
103
  files[".env.example"] = envExample(app)
95
104
  }
105
+ if (app.nativeUpdate) Object.assign(files, nativeUpdateFiles(app))
96
106
  if (app.isTypeScript) {
97
107
  files["types/index.d.ts"] = typesSource(app)
98
108
  }
@@ -140,6 +150,10 @@ function addDependencies(pkg, app) {
140
150
  CAPACITOR_FIREBASE_VERSION
141
151
  pkg.dependencies["@supabase/supabase-js"] = SUPABASE_VERSION
142
152
  }
153
+ if (app.nativeUpdate) {
154
+ pkg.dependencies["@capacitor/app"] = CAPACITOR_APP_VERSION
155
+ pkg.dependencies["@inglorious/supabase"] = "workspace:*"
156
+ }
143
157
  }
144
158
 
145
159
  function catalogue(app, language) {
@@ -148,6 +162,7 @@ function catalogue(app, language) {
148
162
  ...LANGUAGES[language].texts,
149
163
  ...(app.analytics && { analytics }),
150
164
  ...(app.push && { push }),
165
+ ...(app.nativeUpdate && { nativeUpdate: NATIVE_UPDATE_TEXTS[language] }),
151
166
  }
152
167
  }
153
168
 
@@ -195,12 +210,22 @@ function mainSource(app) {
195
210
  if (app.hasPrompts) {
196
211
  imports.push('import { renderPrompts } from "./common/prompt"')
197
212
  }
213
+ if (app.nativeUpdate) {
214
+ imports.push(
215
+ 'import { App as NativeApp } from "@capacitor/app"',
216
+ 'import { when } from "@inglorious/web/directives/when"',
217
+ 'import { watchNativeUpdate } from "@inglorious/web/native-update"',
218
+ )
219
+ if (app.isTypeScript) imports.push('import type { AppApi } from "../types"')
220
+ }
198
221
 
199
222
  const args = [
200
223
  "store",
201
- app.hasPrompts
202
- ? "(api) => html`${App.render(null, api)} ${renderPrompts(api)}`"
203
- : "(api) => App.render(null, api)",
224
+ app.nativeUpdate
225
+ ? "renderApp"
226
+ : app.hasPrompts
227
+ ? "(api) => html`${App.render(null, api)} ${renderPrompts(api)}`"
228
+ : "(api) => App.render(null, api)",
204
229
  app.isTypeScript
205
230
  ? 'document.getElementById("root")!'
206
231
  : 'document.getElementById("root")',
@@ -209,9 +234,7 @@ function mainSource(app) {
209
234
  const body = app.i18n
210
235
  ? `// Mounts once the starting language has loaded, so the first render is
211
236
  // already translated.
212
- ready.then(() =>
213
- ${call("mount", args, " ", ",")}
214
- )`
237
+ ${afterReady(args)}`
215
238
  : call("mount", args, "", "")
216
239
 
217
240
  const session = app.push
@@ -222,12 +245,35 @@ supabase.auth.onAuthStateChange((_event, session) => {
222
245
  })`
223
246
  : ""
224
247
 
248
+ const nativeUpdate = app.nativeUpdate
249
+ ? `
250
+
251
+ watchNativeUpdate(NativeApp, store)
252
+
253
+ // An outdated native build shows nothing but the update page.
254
+ function renderApp(api${app.isTypeScript ? ": AppApi" : ""}) {
255
+ return html\`\${when(
256
+ api.getEntity("nativeUpdate")${app.isTypeScript ? "!" : ""}.isUpdateRequired,
257
+ () => api.render("nativeUpdate"),
258
+ () => html\`\${App.render(null, api)} \${renderPrompts(api)}\`,
259
+ )}\`
260
+ }`
261
+ : ""
262
+
225
263
  return `${importBlock(imports)}
226
264
 
227
- ${body}${session}
265
+ ${body}${session}${nativeUpdate}
228
266
  `
229
267
  }
230
268
 
269
+ function afterReady(args) {
270
+ const oneLine = `ready.then(() => ${call("mount", args, "", "")})`
271
+ if (!oneLine.includes("\n") && oneLine.length <= LINE_WIDTH) return oneLine
272
+ return `ready.then(() =>
273
+ ${call("mount", args, " ", ",")}
274
+ )`
275
+ }
276
+
231
277
  function uiComponents(app) {
232
278
  const names = ["app-bar", "container"]
233
279
  if (app.links) names.push("link")
@@ -244,6 +290,18 @@ body {
244
290
  }
245
291
  `
246
292
 
293
+ const UPDATE_PAGE = `
294
+ .app-update {
295
+ display: flex;
296
+ flex-direction: column;
297
+ align-items: center;
298
+ justify-content: center;
299
+ gap: 1rem;
300
+ padding: 1.5rem;
301
+ text-align: center;
302
+ }
303
+ `
304
+
247
305
  const PLAIN_PROMPT = `
248
306
  .app-prompt {
249
307
  position: fixed;
@@ -287,7 +345,7 @@ main {
287
345
  align-items: center;
288
346
  gap: 1rem;
289
347
  }
290
- ${app.tabs && app.hasPrompts ? PROMPT_ABOVE_TABS : ""}`
348
+ ${app.tabs && app.hasPrompts ? PROMPT_ABOVE_TABS : ""}${app.nativeUpdate ? UPDATE_PAGE : ""}`
291
349
  if (app.ui) {
292
350
  return app.isVue
293
351
  ? `${layout}
@@ -348,12 +406,18 @@ function storeSource(app) {
348
406
  `import { PushPrompt } from "${relative("src/store", app.layout.pushPrompt)}"`,
349
407
  )
350
408
  }
409
+ if (app.nativeUpdate) {
410
+ imports.push(
411
+ `import { NativeUpdate } from "${relative("src/store", app.layout.nativeUpdate)}"`,
412
+ )
413
+ }
351
414
 
352
415
  const types = ["Message", "Home"]
353
416
  if (app.routing) types.push("About", "Router")
354
417
  if (app.i18n) types.push("I18n")
355
418
  if (app.analytics) types.push("Analytics")
356
419
  if (app.push) types.push("PushPrompt")
420
+ if (app.nativeUpdate) types.push("NativeUpdate")
357
421
  const oneLine = ` types: { ${types.join(", ")} },`
358
422
  const typesValue =
359
423
  oneLine.length <= LINE_WIDTH
@@ -381,6 +445,7 @@ function singletons(app) {
381
445
  if (app.i18n) ids.push("i18n")
382
446
  if (app.analytics) ids.push("analytics")
383
447
  if (app.push) ids.push("pushPrompt")
448
+ if (app.nativeUpdate) ids.push("nativeUpdate")
384
449
  return ids
385
450
  }
386
451
 
@@ -527,6 +592,22 @@ function typesSource(app) {
527
592
  )
528
593
  state.push(" pushPrompt: AppPushPromptEntity")
529
594
  }
595
+ if (app.nativeUpdate) {
596
+ imports.push(
597
+ `import type {
598
+ NativeUpdateEntity,
599
+ NativeUpdateType,
600
+ } from "@inglorious/web/native-update"`,
601
+ )
602
+ eventTypes.push("NativeUpdateType")
603
+ entities.push("AppNativeUpdateEntity")
604
+ declarations.push(
605
+ `export interface AppNativeUpdateEntity extends NativeUpdateEntity {
606
+ type: "NativeUpdate"
607
+ }`,
608
+ )
609
+ state.push(" nativeUpdate: AppNativeUpdateEntity")
610
+ }
530
611
 
531
612
  return `${importBlock(imports)}
532
613
 
@@ -1,3 +1,4 @@
1
+ export const CAPACITOR_APP_VERSION = "^8.1.2"
1
2
  export const CAPACITOR_VERSION = "^8.5.3"
2
3
  export const CHANGESETS_VERSION = "^3.0.3"
3
4
  export const CAPACITOR_FIREBASE_VERSION = "^8.5.2"
@@ -272,3 +272,62 @@ function from(name, source) {
272
272
  function language(title, texts) {
273
273
  return { title, texts }
274
274
  }
275
+
276
+ export const NATIVE_UPDATE_TEXTS = {
277
+ en: {
278
+ title: "Time to update",
279
+ body: "This version of the app is no longer supported. Update it to keep going.",
280
+ bodyNoLink:
281
+ "This version of the app is no longer supported. Update it from your app store to keep going.",
282
+ update: "Update",
283
+ },
284
+ it: {
285
+ title: "È ora di aggiornare",
286
+ body: "Questa versione dell'app non è più supportata. Aggiornala per continuare.",
287
+ bodyNoLink:
288
+ "Questa versione dell'app non è più supportata. Aggiornala dal tuo store per continuare.",
289
+ update: "Aggiorna",
290
+ },
291
+ fr: {
292
+ title: "Mise à jour nécessaire",
293
+ body: "Cette version de l'app n'est plus prise en charge. Mettez-la à jour pour continuer.",
294
+ bodyNoLink:
295
+ "Cette version de l'app n'est plus prise en charge. Mettez-la à jour depuis votre store pour continuer.",
296
+ update: "Mettre à jour",
297
+ },
298
+ de: {
299
+ title: "Zeit für ein Update",
300
+ body: "Diese Version der App wird nicht mehr unterstützt. Aktualisiere sie, um weiterzumachen.",
301
+ bodyNoLink:
302
+ "Diese Version der App wird nicht mehr unterstützt. Aktualisiere sie in deinem App Store, um weiterzumachen.",
303
+ update: "Aktualisieren",
304
+ },
305
+ es: {
306
+ title: "Hora de actualizar",
307
+ body: "Esta versión de la app ya no es compatible. Actualízala para continuar.",
308
+ bodyNoLink:
309
+ "Esta versión de la app ya no es compatible. Actualízala desde tu tienda de apps para continuar.",
310
+ update: "Actualizar",
311
+ },
312
+ pt: {
313
+ title: "Hora de atualizar",
314
+ body: "Esta versão da app já não é suportada. Atualiza-a para continuar.",
315
+ bodyNoLink:
316
+ "Esta versão da app já não é suportada. Atualiza-a na tua loja de apps para continuar.",
317
+ update: "Atualizar",
318
+ },
319
+ ja: {
320
+ title: "アップデートが必要です",
321
+ body: "このバージョンのアプリはサポートが終了しました。引き続き使うにはアップデートしてください。",
322
+ bodyNoLink:
323
+ "このバージョンのアプリはサポートが終了しました。引き続き使うにはアプリストアからアップデートしてください。",
324
+ update: "アップデート",
325
+ },
326
+ ar: {
327
+ title: "حان وقت التحديث",
328
+ body: "لم يعد هذا الإصدار من التطبيق مدعومًا. حدّثه للمتابعة.",
329
+ bodyNoLink:
330
+ "لم يعد هذا الإصدار من التطبيق مدعومًا. حدّثه من متجر التطبيقات للمتابعة.",
331
+ update: "تحديث",
332
+ },
333
+ }
package/bin/lib/file.js CHANGED
@@ -8,6 +8,9 @@ import {
8
8
  FIRST_ITEM_INDEX,
9
9
  } from "./constants.js"
10
10
 
11
+ const MIGRATION_STAMP_LENGTH = 14
12
+ const ONE_SECOND = 1
13
+
11
14
  export function copyDir(src, dest) {
12
15
  fs.mkdirSync(dest, { recursive: true })
13
16
 
@@ -64,6 +67,28 @@ export function injectAfterImport(source, importSource, newImport) {
64
67
  )
65
68
  }
66
69
 
70
+ /**
71
+ * Prefixes a migration with the current time, as `supabase migration new`
72
+ * does, a second later than any migration already there.
73
+ *
74
+ * @param {string} targetDir
75
+ * @param {string} name - The file in supabase/migrations.
76
+ */
77
+ export function stampMigration(targetDir, name) {
78
+ const migrations = path.join(targetDir, "supabase/migrations")
79
+ const taken = new Set(
80
+ fs.readdirSync(migrations).map((file) => file.split("_")[FIRST_ITEM_INDEX]),
81
+ )
82
+ const time = new Date()
83
+ while (taken.has(migrationStamp(time))) {
84
+ time.setSeconds(time.getSeconds() + ONE_SECOND)
85
+ }
86
+ fs.renameSync(
87
+ path.join(migrations, name),
88
+ path.join(migrations, `${migrationStamp(time)}_${name}`),
89
+ )
90
+ }
91
+
67
92
  export function escapeRegExp(value) {
68
93
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
69
94
  }
@@ -86,3 +111,10 @@ export function toJavaIdentifier(value) {
86
111
 
87
112
  return identifier || "app"
88
113
  }
114
+
115
+ function migrationStamp(time) {
116
+ return time
117
+ .toISOString()
118
+ .replace(/\D/g, "")
119
+ .slice(FIRST_ITEM_INDEX, MIGRATION_STAMP_LENGTH)
120
+ }
@@ -2,10 +2,8 @@ import fs from "fs"
2
2
  import { EOL } from "os"
3
3
  import path from "path"
4
4
 
5
- import { FIRST_ITEM_INDEX } from "./constants.js"
6
- import { copyDir } from "./file.js"
5
+ import { copyDir, stampMigration } from "./file.js"
7
6
 
8
- const MIGRATION_STAMP_LENGTH = 14
9
7
  const FIRST_NUMBER = 1
10
8
 
11
9
  /**
@@ -27,7 +25,7 @@ export function addFirebaseSupport(targetDir, pkg, options) {
27
25
 
28
26
  if (push) {
29
27
  copyDir(path.join(templatesRoot, "features/push"), targetDir)
30
- stampMigration(targetDir)
28
+ stampMigration(targetDir, "push.sql")
31
29
  pkg.scripts["vault:push"] = "node scripts/push-vault-secrets.mjs"
32
30
  keepSupabaseOutOfTypeScript(targetDir)
33
31
  }
@@ -63,18 +61,6 @@ function replaceInFile(file, search, replacement) {
63
61
  )
64
62
  }
65
63
 
66
- function stampMigration(targetDir) {
67
- const migrations = path.join(targetDir, "supabase/migrations")
68
- const stamp = new Date()
69
- .toISOString()
70
- .replace(/\D/g, "")
71
- .slice(FIRST_ITEM_INDEX, MIGRATION_STAMP_LENGTH)
72
- fs.renameSync(
73
- path.join(migrations, "push.sql"),
74
- path.join(migrations, `${stamp}_push.sql`),
75
- )
76
- }
77
-
78
64
  function appendSetupToReadme(targetDir, { analytics, push, mobile }) {
79
65
  const readmePath = path.join(targetDir, "README.md")
80
66
  if (!fs.existsSync(readmePath)) return
@@ -0,0 +1,119 @@
1
+ import fs from "fs"
2
+ import { EOL } from "os"
3
+ import path from "path"
4
+
5
+ import { copyDir, stampMigration } from "./file.js"
6
+ import { importBlock } from "./imports.js"
7
+ import { relative } from "./source.js"
8
+
9
+ const README_SECTION = `## Mandatory Native Updates
10
+
11
+ The native apps carry their web code inside the binary, so only the stores update them. Each platform's row in the \`app_versions\` table holds the oldest build allowed to run: an older one shows nothing but an update page, at launch and whenever it comes back to the foreground.
12
+
13
+ Once a new build is live in a store, raise that platform's minimum to its build number:
14
+
15
+ \`\`\`sql
16
+ update public.app_versions set min_build = 7 where platform = 'android';
17
+ \`\`\`
18
+
19
+ Never before: users would be blocked with nothing to download. Set \`store_url\` to the store page, or the update page shows no button. A failed check keeps the last answer, so nobody is blocked for being offline.
20
+
21
+ Older builds keep talking to the backend until they are blocked, so a change that breaks them ships in two releases: a build that works with both backends, made mandatory, then the breaking change.`
22
+
23
+ /**
24
+ * The source of the NativeUpdate type: the page that blocks a native build
25
+ * older than the minimum in Supabase. Lit-html in every syntax, like the
26
+ * prompts: src/main renders it in place of the app.
27
+ *
28
+ * @param {{ ui: boolean, i18n: boolean, ext: string, isTypeScript: boolean, layout: Record<string, string> }} app
29
+ * @returns {Record<string, string>} Sources by file path.
30
+ */
31
+ export function nativeUpdateFiles(app) {
32
+ return {
33
+ [`${app.layout.nativeUpdate}/index.${app.ext}`]: nativeUpdateSource(app),
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Adds what mandatory native updates need outside src/: the app_versions
39
+ * table and how to raise its minimum in the README.
40
+ *
41
+ * @param {string} targetDir
42
+ * @param {string} templatesRoot
43
+ */
44
+ export function addNativeUpdateSupport(targetDir, templatesRoot) {
45
+ copyDir(path.join(templatesRoot, "features/native-update"), targetDir)
46
+ stampMigration(targetDir, "native_update.sql")
47
+ appendToReadme(targetDir)
48
+ }
49
+
50
+ function nativeUpdateSource(app) {
51
+ const at = app.layout.nativeUpdate
52
+ const imports = [
53
+ 'import { supabaseNativeUpdate } from "@inglorious/supabase"',
54
+ 'import { html } from "@inglorious/web"',
55
+ 'import { when } from "@inglorious/web/directives/when"',
56
+ 'import { createNativeUpdate } from "@inglorious/web/native-update"',
57
+ `import { supabase } from "${relative(at, "src/api/supabase")}"`,
58
+ ]
59
+ if (app.ui) imports.push('import { Button } from "@inglorious/ui/button"')
60
+ if (app.i18n) imports.push(`import { t } from "${relative(at, "src/i18n")}"`)
61
+ if (app.isTypeScript) {
62
+ imports.push(
63
+ `import type { AppApi, AppNativeUpdateEntity } from "${relative(at, "types")}"`,
64
+ )
65
+ }
66
+ const label = (key, text) =>
67
+ app.i18n ? `t("nativeUpdate.${key}")` : JSON.stringify(text)
68
+ const text = (key, plain) =>
69
+ app.i18n ? `\${t("nativeUpdate.${key}")}` : plain
70
+ const params = app.isTypeScript
71
+ ? "entity: AppNativeUpdateEntity, api: AppApi"
72
+ : "entity, api"
73
+ const button = app.ui
74
+ ? `\${when(entity.storeUrl, () =>
75
+ Button.render({
76
+ children: ${label("update", "Update")},
77
+ color: "primary",
78
+ onClick: () => api.notify(\`#\${entity.id}:nativeUpdateApply\`),
79
+ }),
80
+ )}`
81
+ : `\${when(
82
+ entity.storeUrl,
83
+ () =>
84
+ html\`<button
85
+ @click=\${() => api.notify(\`#\${entity.id}:nativeUpdateApply\`)}
86
+ >
87
+ ${text("update", "Update")}
88
+ </button>\`,
89
+ )}`
90
+
91
+ return `${importBlock(imports)}
92
+
93
+ export const NativeUpdate = {
94
+ ...createNativeUpdate({ adapter: supabaseNativeUpdate(supabase) }),
95
+
96
+ render(${params}) {
97
+ const body = entity.storeUrl
98
+ ? ${label("body", "This version of the app is no longer supported. Update it to keep going.")}
99
+ : ${label("bodyNoLink", "This version of the app is no longer supported. Update it from your app store to keep going.")}
100
+
101
+ return html\`<main class="app-update">
102
+ <h1>${text("title", "Time to update")}</h1>
103
+ <p>\${body}</p>
104
+ ${button}
105
+ </main>\`
106
+ },
107
+ }
108
+ `
109
+ }
110
+
111
+ function appendToReadme(targetDir) {
112
+ const readmePath = path.join(targetDir, "README.md")
113
+ if (!fs.existsSync(readmePath)) return
114
+ const readme = fs.readFileSync(readmePath, "utf-8")
115
+ fs.writeFileSync(
116
+ readmePath,
117
+ `${readme.trimEnd()}${EOL}${EOL}${README_SECTION}${EOL}`,
118
+ )
119
+ }
package/bin/lib/skills.js CHANGED
@@ -18,6 +18,7 @@ export function skillsFor({
18
18
  const skills = [
19
19
  "forge",
20
20
  "forge-best-practices",
21
+ "app-recipe",
21
22
  "store",
22
23
  "web",
23
24
  "web-best-practices",
package/bin/lib/source.js CHANGED
@@ -42,6 +42,7 @@ export function layout(app) {
42
42
  message: `${screens}/home/message`,
43
43
  analytics: "src/app/analytics",
44
44
  pushPrompt: "src/app/push-prompt",
45
+ nativeUpdate: "src/app/native-update",
45
46
  }
46
47
  }
47
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/create-app",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "A scaffolding tool to quickly create a new app with Inglorious Web.",
5
5
  "author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -0,0 +1,17 @@
1
+ -- The oldest native build allowed to run on each platform. Readable before
2
+ -- sign-in, so an outdated app is stopped on its login screen too.
3
+ create table public.app_versions (
4
+ platform text primary key check (platform in ('ios', 'android')),
5
+ min_build integer not null check (min_build >= 1),
6
+ store_url text
7
+ );
8
+
9
+ alter table public.app_versions enable row level security;
10
+
11
+ create policy "Anyone reads the minimum app versions"
12
+ on public.app_versions for select to anon, authenticated
13
+ using (true);
14
+
15
+ insert into public.app_versions (platform, min_build) values
16
+ ('ios', 1),
17
+ ('android', 1);