umpordez 1.1.0 → 1.3.1

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.
Files changed (38) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/README.md +42 -1
  3. package/cli.mjs +4 -2
  4. package/create.mjs +72 -6
  5. package/package.json +1 -1
  6. package/template/AGENTS.md +80 -1
  7. package/template/CLAUDE.md +1 -1
  8. package/template/misc/build-local.sh +82 -0
  9. package/template/mobile/{{PROJECT_SLUG}}/App.tsx +29 -13
  10. package/template/mobile/{{PROJECT_SLUG}}/CHANGELOG.md +28 -0
  11. package/template/mobile/{{PROJECT_SLUG}}/CLAUDE.md +21 -5
  12. package/template/mobile/{{PROJECT_SLUG}}/app.json +3 -0
  13. package/template/mobile/{{PROJECT_SLUG}}/docs/releases.md +78 -0
  14. package/template/mobile/{{PROJECT_SLUG}}/docs/screenshots.md +84 -0
  15. package/template/mobile/{{PROJECT_SLUG}}/docs/store-copy.md +122 -0
  16. package/template/mobile/{{PROJECT_SLUG}}/docs/store-listing.md +107 -0
  17. package/template/mobile/{{PROJECT_SLUG}}/index.ts +47 -1
  18. package/template/mobile/{{PROJECT_SLUG}}/package.json +21 -17
  19. package/template/mobile/{{PROJECT_SLUG}}/src/components/AnimatedSplash.tsx +10 -0
  20. package/template/mobile/{{PROJECT_SLUG}}/src/config.ts +6 -0
  21. package/template/mobile/{{PROJECT_SLUG}}/src/contexts/AuthContext.tsx +2 -3
  22. package/template/scripts/README.md +18 -1
  23. package/template/scripts/appstore-resize.sh +70 -0
  24. package/template/scripts/frame-shots.mjs +246 -0
  25. package/template/scripts/gen-mockups.mjs +120 -0
  26. package/template/scripts/screenshots-capture.sh +50 -0
  27. package/template/scripts/screenshots.flow.yaml +54 -0
  28. package/template/ui/admin/CLAUDE.md +2 -2
  29. package/template/ui/admin/src/lib/fetch-api.ts +19 -7
  30. package/template/ui/site/_variants/app/en.ts +131 -0
  31. package/template/ui/site/_variants/app/footer.ejs +46 -0
  32. package/template/ui/site/_variants/app/header.ejs +102 -0
  33. package/template/ui/site/_variants/app/home.ejs +237 -0
  34. package/template/ui/site/_variants/app/not-found.ejs +22 -0
  35. package/template/ui/site/_variants/app/pt-BR.ts +203 -0
  36. package/template/ui/site/tailwind.config.js +9 -1
  37. package/template-variables.mjs +34 -0
  38. package/update.mjs +30 -1
@@ -46,7 +46,9 @@
46
46
  "Bash(md5 /tmp/umptest/everything/mobile/testapp/assets/icon.png /Users/ligeiro/dev/tranqs/app/mobile/tranqs/assets/icon.png)",
47
47
  "Bash(git merge *)",
48
48
  "Bash(git tag *)",
49
- "Bash(npm pack *)"
49
+ "Bash(npm pack *)",
50
+ "Read(//Users/ligeiro/dev/dropa/**)",
51
+ "Read(//Users/ligeiro/dev/growerheleper-v2/app/**)"
50
52
  ]
51
53
  }
52
54
  }
package/README.md CHANGED
@@ -98,11 +98,19 @@ It asks **what to generate** first:
98
98
  |---|---|
99
99
  | everything | server + admin UI + site + mobile |
100
100
  | web-only | server + admin UI + site (no mobile) |
101
+ | mobile-and-site | mobile app + app-store landing page (no server, no admin) |
101
102
  | mobile-and-server | server + mobile (no UI) |
102
103
  | just-mobile | mobile only (assumes external API) |
103
104
 
105
+ The **public site** ships two landing flavors and the CLI picks by
106
+ preset: a **SaaS landing** (CTAs → admin signup) for server/admin
107
+ presets, and an **app-store landing** (App Store / Play buttons,
108
+ pricing, download) for `mobile-and-site`, branded from your primary
109
+ color.
110
+
104
111
  Then a few questions (name, domain, ports, primary color, mobile
105
- bundle ID if applicable). After scaffold:
112
+ bundle ID + **splash style** — instant show-up or animated — if
113
+ applicable). After scaffold:
106
114
 
107
115
  ```bash
108
116
  cd my-project
@@ -198,6 +206,39 @@ umpordez --version check version
198
206
  | Uploads | AWS S3 with signed URLs |
199
207
  | Email | Nodemailer + HTML templates |
200
208
 
209
+ ## What changed in 1.2
210
+
211
+ - **Non-Negotiables** section in AGENTS.md — codifies rules per layer
212
+ (server, admin UI, site, mobile) so teams don't have to rediscover
213
+ them.
214
+ - **Key Patterns (TL;DR)** — quick reference for adding entities,
215
+ context DI, route handlers, and transactions.
216
+ - **i18n Coverage** table — shows which layers speak which
217
+ languages at a glance.
218
+ - **Anonymous-first mobile** — synthetic device-account on first
219
+ boot, never gate behind sign-in.
220
+ - **fetch-api** now supports PUT and PATCH methods plus safer
221
+ URL parameter filtering (skips null/undefined values).
222
+
223
+ ## What's new in 1.3
224
+
225
+ Ship a mobile app + its marketing site in one shot:
226
+
227
+ - **`mobile-and-site` preset** — mobile app + app-store landing page,
228
+ no server or admin. The "indie mobile app" shape.
229
+ - **App-store landing** — App Store / Play buttons, phone mockup,
230
+ pricing, download CTAs; branded from your primary color. The CLI
231
+ picks SaaS vs app landing by preset (and `umpordez update` keeps
232
+ the right flavor).
233
+ - **Splash choice** — instant show-up (default; native splash hides on
234
+ font load) or the animated splash, chosen at scaffold time.
235
+ - **Store launch kit** (every mobile preset) — `CHANGELOG.md` +
236
+ `docs/store-listing.md`, `store-copy.md`, `releases.md`,
237
+ `screenshots.md`.
238
+ - **Screenshot + release automation** — `frame-shots.mjs`,
239
+ `gen-mockups.mjs`, `screenshots-capture.sh` + Maestro flow,
240
+ `appstore-resize.sh`, and `misc/build-local.sh`.
241
+
201
242
  ## What changed in 1.1
202
243
 
203
244
  The big one. v1.0.x was server + web only. v1.1 adds:
package/cli.mjs CHANGED
@@ -41,13 +41,15 @@ function showHelp() {
41
41
  console.log(` umpordez --version ${dim('Show version')}`);
42
42
  console.log('');
43
43
  console.log(green.bold('What you get:'));
44
+ console.log(` ${green('>')} Presets: everything / web-only / mobile+site / mobile+server / just-mobile`);
44
45
  console.log(` ${green('>')} Multi-tenant auth (cookie-based JWT)`);
45
46
  console.log(` ${green('>')} Admin API + Site API (Express)`);
46
47
  console.log(` ${green('>')} React admin panel (Vite + shadcn/ui)`);
47
48
  console.log(` ${green('>')} PostgreSQL + Knex.js migrations`);
48
49
  console.log(` ${green('>')} S3 file uploads with signed URLs`);
49
- console.log(` ${green('>')} EJS-based public site`);
50
- console.log(` ${green('>')} Console task runner`);
50
+ console.log(` ${green('>')} Public site — SaaS or app-store landing (EJS + i18n + SEO)`);
51
+ console.log(` ${green('>')} Mobile app (Expo + TS) — instant or animated splash`);
52
+ console.log(` ${green('>')} Store launch kit (listing, copy, changelog, screenshots)`);
51
53
  console.log(` ${green('>')} Role-based access (admin/owner/manager/user)`);
52
54
  console.log(` ${green('>')} Production build system with versioning`);
53
55
  console.log('');
package/create.mjs CHANGED
@@ -4,7 +4,11 @@ import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { execSync } from 'node:child_process';
6
6
  import { fileURLToPath } from 'node:url';
7
- import { buildReplacements } from './template-variables.mjs';
7
+ import {
8
+ buildReplacements,
9
+ APP_LANDING_SWAPS,
10
+ SITE_VARIANT_DIR
11
+ } from './template-variables.mjs';
8
12
  import { generateIcons } from './icons.mjs';
9
13
  import { writeInitialManifest } from './update.mjs';
10
14
 
@@ -45,6 +49,17 @@ const PRESETS = {
45
49
  wantsAdminUi: true,
46
50
  wantsSite: true,
47
51
  },
52
+ 'mobile-and-site': {
53
+ label: 'Mobile app + landing page (no server, no admin)',
54
+ includes: ['mobile', 'ui/site', 'scripts', 'doc'],
55
+ wantsMobile: true,
56
+ wantsServer: false,
57
+ wantsAdminUi: false,
58
+ wantsSite: true,
59
+ // Swaps the SaaS landing for the app-marketing landing
60
+ // (App Store / Play buttons, pricing, download CTA).
61
+ appLanding: true,
62
+ },
48
63
  'mobile-and-server': {
49
64
  label: 'Mobile + server only (no admin UI, no site)',
50
65
  includes: ['server', 'mobile', 'misc', 'scripts', 'doc'],
@@ -70,6 +85,8 @@ const ROOT_FILES_BY_PRESET = {
70
85
  'web-only': ['CLAUDE.md', 'AGENTS.md', 'README.md',
71
86
  'architecture.md', 'code.md',
72
87
  'install.sh', 'seed.sh', 'dev.sh'],
88
+ 'mobile-and-site': ['CLAUDE.md', 'AGENTS.md', 'README.md',
89
+ 'code.md', 'install.sh', 'dev.sh'],
73
90
  'mobile-and-server': ['CLAUDE.md', 'AGENTS.md', 'README.md',
74
91
  'architecture.md', 'code.md',
75
92
  'install.sh', 'seed.sh', 'dev.sh'],
@@ -266,6 +283,16 @@ async function promptProjectDetails() {
266
283
  ? true
267
284
  : 'Lowercase alphanumeric, dashes allowed',
268
285
  },
286
+ {
287
+ type: 'list',
288
+ name: 'splashStyle',
289
+ message: 'Splash screen:',
290
+ choices: [
291
+ { name: 'Instant show-up (recommended)', value: 'instant' },
292
+ { name: 'Animated (glow + logo + pulse)', value: 'animated' },
293
+ ],
294
+ default: 'instant',
295
+ },
269
296
  ];
270
297
 
271
298
  const answers = await inquirer.prompt([
@@ -296,6 +323,7 @@ async function promptProjectDetails() {
296
323
  androidPackage: answers.androidPackage
297
324
  ?? toReverseDomain(answers.domain),
298
325
  mobileScheme: answers.mobileScheme ?? answers.slug,
326
+ splashStyle: answers.splashStyle ?? 'instant',
299
327
  ...answers,
300
328
  };
301
329
  }
@@ -331,17 +359,28 @@ function showSummary(details) {
331
359
  }
332
360
  console.log(` ${chalk.bold('Color:')} ${chalk.hex(details.primaryColor)('██')} ${details.primaryColor}`);
333
361
  console.log(` ${chalk.bold('Theme:')} ${details.defaultTheme}`);
362
+ if (presetDef.wantsSite) {
363
+ console.log(` ${chalk.bold('Landing:')} ${cyan(
364
+ presetDef.appLanding ? 'app store (download)' : 'SaaS (signup)'
365
+ )}`);
366
+ }
334
367
  if (presetDef.wantsMobile) {
335
368
  console.log(` ${chalk.bold('iOS:')} ${details.iosBundleId}`);
336
369
  console.log(` ${chalk.bold('Android:')} ${details.androidPackage}`);
337
370
  console.log(` ${chalk.bold('Scheme:')} ${details.mobileScheme}://`);
371
+ console.log(` ${chalk.bold('Splash:')} ${details.splashStyle === 'animated'
372
+ ? 'animated (glow + logo)'
373
+ : 'instant show-up'}`);
338
374
  }
339
375
  console.log('');
340
376
  console.log(dim(' Generates:'));
341
377
  if (presetDef.wantsServer) console.log(dim(` ${green('>')} Admin API + Site API (Express + TS)`));
342
378
  if (presetDef.wantsAdminUi) console.log(dim(` ${green('>')} React admin panel (Vite + shadcn/ui)`));
343
- if (presetDef.wantsSite) console.log(dim(` ${green('>')} Public site (EJS + Tailwind)`));
379
+ if (presetDef.wantsSite) console.log(dim(` ${green('>')} ${presetDef.appLanding
380
+ ? 'App landing page (store buttons + pricing)'
381
+ : 'Public site (EJS + Tailwind)'}`));
344
382
  if (presetDef.wantsMobile) console.log(dim(` ${green('>')} Mobile app (Expo + TS + auth + theme)`));
383
+ if (presetDef.wantsMobile) console.log(dim(` ${green('>')} Store launch kit (listing, copy, changelog, screenshots)`));
345
384
  if (presetDef.wantsServer) console.log(dim(` ${green('>')} PostgreSQL migrations + seed`));
346
385
  console.log('');
347
386
  }
@@ -383,7 +422,7 @@ function replacePlaceholders(content, replacements) {
383
422
  // config, git ignore) doesn't belong to any single sub-app.
384
423
  const ALWAYS_KEEP = new Set(['.claude']);
385
424
 
386
- async function copyTemplate(outputDir, preset) {
425
+ export async function copyTemplate(outputDir, preset) {
387
426
  const presetDef = PRESETS[preset];
388
427
  await fs.promises.cp(TEMPLATE_DIR, outputDir, { recursive: true });
389
428
 
@@ -442,6 +481,25 @@ async function copyTemplate(outputDir, preset) {
442
481
  await fs.promises.rm(path.join(outputDir, entry.name));
443
482
  }
444
483
  }
484
+
485
+ // Landing-page variant swap. App-focused presets replace the SaaS
486
+ // landing with the app-marketing one before placeholder replacement
487
+ // runs (so the swapped files get their placeholders filled too). The
488
+ // _variants staging dir is always removed — it never ships.
489
+ if (presetDef.appLanding) {
490
+ for (const [variant, canonical] of APP_LANDING_SWAPS) {
491
+ const from = path.join(outputDir, variant);
492
+ const to = path.join(outputDir, canonical);
493
+ if (fs.existsSync(from)) {
494
+ await fs.promises.rm(to, { force: true });
495
+ await fs.promises.rename(from, to);
496
+ }
497
+ }
498
+ }
499
+ await fs.promises.rm(
500
+ path.join(outputDir, SITE_VARIANT_DIR),
501
+ { recursive: true, force: true }
502
+ );
445
503
  }
446
504
 
447
505
  export async function createProject() {
@@ -565,9 +623,14 @@ export async function createProject() {
565
623
  'scripts/run-ios.sh',
566
624
  'scripts/release-android.sh',
567
625
  'scripts/generate-icons.sh',
568
- 'scripts/install-native.sh'
626
+ 'scripts/install-native.sh',
627
+ 'scripts/appstore-resize.sh',
628
+ 'scripts/screenshots-capture.sh'
569
629
  );
570
630
  }
631
+ if (details.presetDef.wantsServer) {
632
+ scripts.push('misc/build-local.sh');
633
+ }
571
634
 
572
635
  for (const script of scripts) {
573
636
  const scriptPath = path.join(outputDir, script);
@@ -638,8 +701,10 @@ function showNextSteps(details) {
638
701
  console.log(` ${green('3.')} ./seed.sh`);
639
702
  console.log(` ${green('4.')} ./dev.sh`);
640
703
  } else {
641
- console.log(` ${green('2.')} cd mobile/${details.slug} && npm install`);
642
- console.log(` ${green('3.')} npm run start`);
704
+ // No DB to seed, but install.sh + dev.sh still orchestrate the
705
+ // site + mobile (they guard on directory existence).
706
+ console.log(` ${green('2.')} ./install.sh`);
707
+ console.log(` ${green('3.')} ./dev.sh`);
643
708
  }
644
709
  console.log('');
645
710
  if (presetDef.wantsAdminUi) {
@@ -653,6 +718,7 @@ function showNextSteps(details) {
653
718
  }
654
719
  if (presetDef.wantsMobile) {
655
720
  console.log(dim(` Mobile: cd mobile/${details.slug} && npm run ios | android | web`));
721
+ console.log(dim(` Store kit: mobile/${details.slug}/docs/ (listing, copy, screenshots) + CHANGELOG.md`));
656
722
  }
657
723
  console.log('');
658
724
  console.log(dim(' Happy hacking! - ') + cyan('youtube.com/ligeiro'));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "umpordez",
3
3
  "type": "module",
4
- "version": "1.1.0",
4
+ "version": "1.3.1",
5
5
  "description": "SaaS starter kit generator — server, admin SPA, public site (i18n), mobile app (offline-first, IAP, native push)",
6
6
  "main": "cli.mjs",
7
7
  "scripts": {
@@ -276,7 +276,86 @@ const ok = await confirm({
276
276
  if (!ok) return;
277
277
  ```
278
278
 
279
- ## Multi-Tenancy
279
+ ## Non-Negotiables
280
+
281
+ ### Code Style
282
+ - 4-space indent, semicolons, max 80 cols, LF, always braces
283
+ - TypeScript strict (no `any`, use `unknown`)
284
+ - Code in English; user-facing strings in Portuguese (pt-BR)
285
+ - Path aliases: `#core/*`, `#shared/*`, `#api/*` (server), `@/*` (admin UI / mobile)
286
+ - No relative paths beyond single `../`
287
+
288
+ ### Server
289
+ - Routes go through models (never `req.context.knex()` in routes)
290
+ - `buildHandler()` wraps every async handler; Zod validates input
291
+ - Raw SQL in migrations (`knex.schema.raw()`)
292
+ - Models extend `BaseModel` — get `this.knex`, `this.context`, `this.db`
293
+ - Errors: `ValidationError` (400), `NotFoundError` (404), `ConflictError` (409), `ForbiddenError` (403)
294
+ - Pagination: `{ rows, total, page, limit, totalPages }`
295
+ - Every table: `id UUID`, `utc_created_on`, `utc_updated_on`; multi-tenant tables also `account_id`
296
+
297
+ ### Admin UI
298
+ - `fetchApi()` for every API call (handles cookies, JSON, FormData)
299
+ - React Query for all server state; `keepPreviousData` on paginated queries
300
+ - Query keys centralized in `lib/query-keys.ts`
301
+ - shadcn/ui primitives under `components/ui/` (compose, don't fork)
302
+ - `useConfirm()` instead of `window.confirm()`, `toast` from sonner
303
+
304
+ ### Site
305
+ - Every string through `t()` — no bare text in templates
306
+ - URL strategy: `/` → pt-BR, `/en/...` → English (middleware strips prefix)
307
+ - Hreflang alternates + canonical + sitemap.xml on every page
308
+
309
+ ### Mobile
310
+ - **Free-first**: never gate behind sign-in; anonymous usage is the default
311
+ - **Anonymous-first**: synthetic UUID device-account on first boot, never gate behind login
312
+ - **Services own data access** — pages never call `getDb()` directly
313
+ - **User-scoped writes check `demandSigned()`** — reads always work, writes no-op when gate closed
314
+ - Every string through `t()` — no bare text in JSX
315
+ - NitroModules crash in Expo Go — lazy-load with `try/require`
316
+ - Never `expo prebuild --clean` (wipes native code; use `scripts/prebuild.sh`)
317
+
318
+ ### i18n Coverage
319
+ | Layer | pt-BR | en | Additional |
320
+ |-------|-------|----|------------|
321
+ | Server | ✓ | ✓ | Extensible via i18next |
322
+ | Admin UI | ✓ | ✗ | pt-BR only (Brazil market) |
323
+ | Site | ✓ | ✓ | URL-prefixed (/ vs /en/) |
324
+ | Mobile | ✓ | ✓ | Extensible per project |
325
+
326
+ ## Key Patterns (TL;DR)
327
+
328
+ ### Adding a domain entity
329
+ 1. `migrations/YYYYMMDDHHMMSS_create-thing.ts` — raw SQL
330
+ 2. `core/models/thing.ts` — extends `BaseModel`
331
+ 3. Register on `Context` in `core/context.ts`
332
+ 4. `apps/api/routes/thing.ts` — `buildHandler()` + Zod
333
+ 5. Admin UI: page + `hooks/queries/use-thing.ts`
334
+ 6. Mobile (optional): `src/services/thing/`
335
+
336
+ ### Context DI
337
+ ```typescript
338
+ const users = await req.context.user.list({ page: 1 });
339
+ this.user = new UserModel(this); // in context.ts
340
+ ```
341
+
342
+ ### Route handler
343
+ ```typescript
344
+ router.post('/things', buildHandler(async (req, res) => {
345
+ const data = z.object({ name: z.string().min(1) }).parse(req.body);
346
+ const result = await req.context.thing.create(req.accountId!, data);
347
+ res.json({ ok: true, result });
348
+ }));
349
+ ```
350
+
351
+ ### Transaction
352
+ ```typescript
353
+ await req.context.knexTransaction(async () => {
354
+ const user = await req.context.user.create(data);
355
+ await req.context.account.addMember(accountId, { ... });
356
+ });
357
+ ```
358
+
280
359
 
281
360
  ```
282
361
  users (global identity) → user_in_accounts (many-to-many + role) → accounts (tenant) → [domain entities]
@@ -58,7 +58,7 @@ URLs:
58
58
  Folders that the preset didn't ship are simply absent. `dev.sh`
59
59
  and `install.sh` guard on existence per directory, so the same
60
60
  script works for every preset (everything / web-only /
61
- mobile-and-server / just-mobile).
61
+ mobile-and-site / mobile-and-server / just-mobile).
62
62
 
63
63
  ## Multi-tenancy model
64
64
 
@@ -0,0 +1,82 @@
1
+ #!/bin/bash
2
+ #
3
+ # build-local — produce a production build of every web sub-project this
4
+ # repo ships (server, admin SPA, public site) into a sibling `builds/`
5
+ # directory, bump a build number and (optionally) commit + push it.
6
+ #
7
+ # Layout produced (matches CLAUDE.md → Deployment):
8
+ # ../builds/server/ server `npm run build` output + deps
9
+ # ../builds/ui/admin/dist/ Vite build (nginx serves this)
10
+ # ../builds/ui/site/ compiled site + views + public
11
+ #
12
+ # Usage (from the project root):
13
+ # ./misc/build-local.sh # build into ../builds
14
+ # ./misc/build-local.sh --push # also git commit + tag + push builds
15
+ #
16
+ # Each block is guarded on directory existence, so the same script works
17
+ # whatever preset generated this project.
18
+ set -euo pipefail
19
+
20
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
21
+ cd "$ROOT"
22
+
23
+ BUILD_DIR="${BUILD_DIR:-../builds}"
24
+ PUSH=0
25
+ for arg in "$@"; do
26
+ case "$arg" in
27
+ --push) PUSH=1 ;;
28
+ esac
29
+ done
30
+
31
+ mkdir -p "$BUILD_DIR"
32
+
33
+ # ── Build number ────────────────────────────────────────────────────
34
+ NUM_FILE="$BUILD_DIR/build-number"
35
+ number=$(cat "$NUM_FILE" 2>/dev/null || echo 0)
36
+ next=$((number + 1))
37
+
38
+ # ── Server ──────────────────────────────────────────────────────────
39
+ if [ -d server ]; then
40
+ echo "==> Building server…"
41
+ (cd server && npm run build)
42
+ mkdir -p "$BUILD_DIR/server"
43
+ rsync -a --delete \
44
+ --exclude node_modules --exclude .env \
45
+ server/dist/ "$BUILD_DIR/server/dist/" 2>/dev/null || true
46
+ cp server/package.json server/package-lock.json "$BUILD_DIR/server/" 2>/dev/null || true
47
+ fi
48
+
49
+ # ── Admin SPA ───────────────────────────────────────────────────────
50
+ if [ -d ui/admin ]; then
51
+ echo "==> Building admin SPA…"
52
+ (cd ui/admin && npm run build)
53
+ mkdir -p "$BUILD_DIR/ui/admin"
54
+ rsync -a --delete ui/admin/dist/ "$BUILD_DIR/ui/admin/dist/"
55
+ fi
56
+
57
+ # ── Public site ─────────────────────────────────────────────────────
58
+ if [ -d ui/site ]; then
59
+ echo "==> Building public site…"
60
+ (cd ui/site && npm run build)
61
+ mkdir -p "$BUILD_DIR/ui/site"
62
+ rsync -a --delete \
63
+ --exclude node_modules --exclude .env \
64
+ ui/site/build/ "$BUILD_DIR/ui/site/build/" 2>/dev/null || true
65
+ cp ui/site/package.json "$BUILD_DIR/ui/site/" 2>/dev/null || true
66
+ cp -R ui/site/views "$BUILD_DIR/ui/site/" 2>/dev/null || true
67
+ cp -R ui/site/public "$BUILD_DIR/ui/site/" 2>/dev/null || true
68
+ fi
69
+
70
+ echo "$next" > "$NUM_FILE"
71
+ echo ""
72
+ echo "Build #$next written to $BUILD_DIR"
73
+
74
+ # ── Optional: commit + push the builds repo ─────────────────────────
75
+ if [ "$PUSH" = "1" ] && [ -d "$BUILD_DIR/.git" ]; then
76
+ echo "==> Committing + pushing builds…"
77
+ (cd "$BUILD_DIR" \
78
+ && git add . \
79
+ && git commit -m "build: #$next" \
80
+ && git tag -a "$next" -m "#$next" \
81
+ && git push && git push origin --tags)
82
+ fi
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { AppState } from 'react-native';
3
3
  import { SafeAreaProvider } from 'react-native-safe-area-context';
4
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
4
5
  import Toast from 'react-native-toast-message';
5
6
  import * as SplashScreen from 'expo-splash-screen';
6
7
 
@@ -14,13 +15,16 @@ import AnimatedSplash from './src/components/AnimatedSplash';
14
15
  import { toastConfig } from './src/components/ToastConfig';
15
16
  import { initDb } from './src/services/db';
16
17
  import { syncAllContent } from './src/services/content';
18
+ import { SPLASH_ANIMATED } from './src/config';
17
19
 
18
20
  // Hide the native splash screen so our AnimatedSplash takes over the
19
21
  // transition. Best-effort — if the call fails (rare), we just continue.
20
22
  SplashScreen.preventAutoHideAsync().catch(() => { /* best-effort */ });
21
23
 
22
24
  export default function App() {
23
- const [splashDone, setSplashDone] = useState<boolean>(false);
25
+ // Instant show-up (SPLASH_ANIMATED = false) starts already "done" so
26
+ // no overlay ever mounts — the app appears the moment fonts load.
27
+ const [splashDone, setSplashDone] = useState<boolean>(!SPLASH_ANIMATED);
24
28
 
25
29
  useEffect(() => {
26
30
  // Init runs in the background. App rendering doesn't wait on
@@ -28,6 +32,13 @@ export default function App() {
28
32
  // `splashDone` flips, and screens read from SQLite which
29
33
  // serves cached data while the network sync runs.
30
34
  SplashScreen.hideAsync().catch(() => { /* best-effort */ });
35
+
36
+ // Force-hide the splash after 5s no matter what, so a stuck
37
+ // animation never keeps the user out of the app. In Release
38
+ // there's no red box — without this timeout a render-time
39
+ // crash is invisible.
40
+ const fallback = setTimeout(() => setSplashDone(true), 5000);
41
+
31
42
  (async () => {
32
43
  try {
33
44
  await initDb();
@@ -55,21 +66,26 @@ export default function App() {
55
66
  });
56
67
  }
57
68
  });
58
- return () => sub.remove();
69
+ return () => {
70
+ sub.remove();
71
+ clearTimeout(fallback);
72
+ };
59
73
  }, []);
60
74
 
61
75
  return (
62
- <SafeAreaProvider>
63
- <ThemeProvider>
64
- <AuthProvider>
65
- <Routes />
66
- <Toast config={toastConfig} />
67
- </AuthProvider>
68
- </ThemeProvider>
76
+ <GestureHandlerRootView style={{ flex: 1 }}>
77
+ <SafeAreaProvider>
78
+ <ThemeProvider>
79
+ <AuthProvider>
80
+ <Routes />
81
+ <Toast config={toastConfig} />
82
+ </AuthProvider>
83
+ </ThemeProvider>
69
84
 
70
- {!splashDone && (
71
- <AnimatedSplash onDone={() => setSplashDone(true)} />
72
- )}
73
- </SafeAreaProvider>
85
+ {SPLASH_ANIMATED && !splashDone && (
86
+ <AnimatedSplash onDone={() => setSplashDone(true)} />
87
+ )}
88
+ </SafeAreaProvider>
89
+ </GestureHandlerRootView>
74
90
  );
75
91
  }
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ All notable changes to **{{PROJECT_NAME}}** (the mobile app). This is the
4
+ source of truth for what shipped; store "What's New" copy is derived from
5
+ it in [`docs/releases.md`](./docs/releases.md).
6
+
7
+ Format loosely follows [Keep a Changelog](https://keepachangelog.com);
8
+ versions match `app.json` (`version` / `ios.buildNumber` /
9
+ `android.versionCode`).
10
+
11
+ ## [Unreleased]
12
+
13
+ ### Added
14
+ - First public build.
15
+
16
+ ### Changed
17
+ -
18
+
19
+ ### Fixed
20
+ -
21
+
22
+ <!--
23
+ When you cut a release:
24
+ 1. `../../scripts/bump-version.sh patch|minor|major`
25
+ 2. Move the items above under a new `## [x.y.z] - YYYY-MM-DD` heading.
26
+ 3. Reset [Unreleased] to empty Added/Changed/Fixed.
27
+ 4. Translate the highlights into store copy in docs/releases.md.
28
+ -->
@@ -6,7 +6,7 @@ auth, 5-tab navigator, offline SQLite, custom-native modules.
6
6
  ```
7
7
  src/
8
8
  ├── App.tsx, index.ts
9
- ├── config.ts API base URL + provider client IDs
9
+ ├── config.ts API base URL + client IDs + SPLASH_ANIMATED
10
10
  ├── theme/colors.ts Palette generated from primary color
11
11
  ├── i18n/ pt-BR default + en (react-i18next)
12
12
  ├── contexts/
@@ -63,10 +63,17 @@ hasCompletedOnboarding
63
63
  render an "auth required" screen at the entry point. Wrap
64
64
  premium features in `<PremiumGate onUpgrade={openAuth} />`.
65
65
 
66
- - **Routes go through models** same rule as server. Mobile
66
+ - **Anonymous-first.** `AuthContext` fabricates a synthetic device-
67
+ account on first boot (`services/auth/anonymous.ts` returns a
68
+ UUID v4 stored in AsyncStorage). `signed: true` always;
69
+ `isAnonymous: true` for the default state. Never gate app entry
70
+ behind sign-in. The opt-in AuthStack lives in Profile's footer
71
+ link only.
72
+
73
+ - **Services own data access** — same rule as server. Mobile
67
74
  doesn't have models; the equivalent is: **services own data
68
- access**. Pages call `getContentByType('post')`, not
69
- `getDb().getAllAsync(...)`. New SQLite reads/writes go in the
75
+ access**. Pages call `services/crops`, `services/events`, … —
76
+ never `getDb()` directly. New SQLite reads/writes go in the
70
77
  matching `services/<thing>/` folder.
71
78
 
72
79
  - **User-scoped writes check `demandSigned()`** at the top of the
@@ -88,6 +95,15 @@ hasCompletedOnboarding
88
95
  Use `./scripts/prebuild.sh` (refuses --clean). To genuinely
89
96
  reset, move customizations into `plugins/` first.
90
97
 
98
+ ## Splash
99
+
100
+ `config.ts` exports `SPLASH_ANIMATED` (set at scaffold time). When
101
+ `false` (instant show-up, the default) the native splash hides the
102
+ moment fonts load and the app appears immediately — `AnimatedSplash`
103
+ never mounts. When `true`, `App.tsx` overlays `AnimatedSplash`
104
+ (glow + logo + pulse ring, ~2s) until it finishes. The component ships
105
+ either way; flip the flag to switch behavior.
106
+
91
107
  ## Adding a screen
92
108
 
93
109
  1. **Page** — `src/pages/<scope>/<Name>.tsx`. Typed via
@@ -150,7 +166,7 @@ Client → /api/auth/google/app
150
166
  ```
151
167
 
152
168
  `signOut()` clears auth + onboarding state so the next user starts
153
- from Welcome (matches tranqs's pattern; onboarding is per-account).
169
+ from Welcome (onboarding is per-account).
154
170
 
155
171
  ## Premium
156
172
 
@@ -48,6 +48,9 @@
48
48
  {
49
49
  "android": {
50
50
  "minSdkVersion": 26
51
+ },
52
+ "ios": {
53
+ "deploymentTarget": "17.0"
51
54
  }
52
55
  }
53
56
  ],