@clubedge/create-clubedge-app 0.46.0 → 0.46.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 (60) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +10 -10
  3. package/package.json +3 -3
  4. package/template/apps/api/package.json +1 -1
  5. package/template/apps/api-fastapi/package.json +1 -1
  6. package/template/apps/api-fastapi/pyproject.toml +1 -1
  7. package/template/apps/api-fastapi/uv.lock +1 -1
  8. package/template/apps/capacitor/capacitor.config.ts +7 -0
  9. package/template/apps/capacitor/e2e/README.md +23 -0
  10. package/template/apps/capacitor/e2e/account.yaml +116 -0
  11. package/template/apps/capacitor/package.json +1 -1
  12. package/template/apps/capacitor/scripts/build.mjs +4 -2
  13. package/template/apps/expo/app.config.ts +18 -1
  14. package/template/apps/expo/e2e/README.md +26 -0
  15. package/template/apps/expo/e2e/account.yaml +111 -0
  16. package/template/apps/expo/package.json +5 -5
  17. package/template/apps/expo/src/lib/api.ts +2 -1
  18. package/template/apps/expo/src/lib/send.ts +27 -0
  19. package/template/apps/expo/src/variants/api.auth.ts +6 -11
  20. package/template/apps/start/package.json +3 -3
  21. package/template/apps/vite/package.json +3 -3
  22. package/template/apps/web/package.json +3 -3
  23. package/template/clubedge.template.json +1 -0
  24. package/template/package.json +1 -1
  25. package/template/packages/auth/package.json +1 -1
  26. package/template/packages/auth-appwrite/package.json +1 -1
  27. package/template/packages/auth-better-auth/package.json +1 -1
  28. package/template/packages/auth-firebase/package.json +1 -1
  29. package/template/packages/auth-supabase/package.json +1 -1
  30. package/template/packages/cache/package.json +1 -1
  31. package/template/packages/cache-redis/package.json +1 -1
  32. package/template/packages/contract/package.json +1 -1
  33. package/template/packages/contract/src/app-update.test.ts +21 -0
  34. package/template/packages/contract/src/app-update.ts +4 -2
  35. package/template/packages/contract/src/index.ts +1 -1
  36. package/template/packages/core/package.json +1 -1
  37. package/template/packages/db/package.json +1 -1
  38. package/template/packages/email/package.json +1 -1
  39. package/template/packages/email-resend/package.json +1 -1
  40. package/template/packages/email-smtp/package.json +1 -1
  41. package/template/packages/jobs/package.json +1 -1
  42. package/template/packages/jobs-bullmq/package.json +1 -1
  43. package/template/packages/jobs-inngest/package.json +1 -1
  44. package/template/packages/jobs-pg-boss/package.json +1 -1
  45. package/template/packages/local-db/package.json +1 -1
  46. package/template/packages/mobile/package.json +5 -5
  47. package/template/packages/observability/package.json +1 -1
  48. package/template/packages/observability-otel/package.json +1 -1
  49. package/template/packages/observability-sentry/package.json +1 -1
  50. package/template/packages/payments/package.json +1 -1
  51. package/template/packages/payments-stripe/package.json +1 -1
  52. package/template/packages/push/package.json +1 -1
  53. package/template/packages/push-expo/package.json +1 -1
  54. package/template/packages/push-native/package.json +1 -1
  55. package/template/packages/storage/package.json +1 -1
  56. package/template/packages/storage-s3/package.json +1 -1
  57. package/template/packages/storage-supabase/package.json +1 -1
  58. package/template/packages/ui/package.json +2 -2
  59. package/template/pnpm-lock.yaml +374 -376
  60. package/template.lock.json +7 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to `create-clubedge-app` are documented here.
4
4
 
5
+ ## 0.46.1
6
+
7
+ - **Starter v0.30.1** by default (#167). Its template schema is still 6. The [Starter's release notes](https://github.com/Clubedge/clubedge-starter/releases/tag/v0.30.1) cover:
8
+ - the native app (`--mobile expo`) reaching its API again: since v0.29.0, each of its calls to the API threw after the API had answered, so the account never loaded, and saving a name or deleting the account failed;
9
+ - getting the fix to installed apps: a new build in the stores, then `APP_MINIMUM_VERSION` on the API, which sends older installs to the update screen, or an over-the-air update with `--ota eas`;
10
+ - end-to-end tests of both mobile apps on an Android emulator;
11
+ - Expo SDK 58's latest patch versions.
12
+
5
13
  ## 0.46.0
6
14
 
7
15
  - **Add and remove the apps installed on devices** (#161), so a project created without them can take them later, and drop them again, without starting over: `pnpm clubedge add mobile expo` for the native app, `pnpm clubedge add shell capacitor` for the shell that packages the website, and `pnpm clubedge remove mobile` or `pnpm clubedge remove shell`. The shell needs Starter v0.27.0 or later, and the native app v0.28.0.
package/README.md CHANGED
@@ -243,7 +243,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
243
243
  pnpm dlx @clubedge/create-clubedge-app my-app --ref main
244
244
 
245
245
  # Reproduce a specific Starter release or commit
246
- pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.30.0
246
+ pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.30.1
247
247
  pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
248
248
 
249
249
  # Test local Starter changes before releasing them
@@ -264,7 +264,7 @@ pnpm clubedge add mobile expo # the native app; ad
264
264
  pnpm clubedge remove mobile
265
265
  pnpm dlx @clubedge/create-clubedge-app add infra local --dry-run
266
266
  pnpm dlx @clubedge/create-clubedge-app@latest upgrade # to the newest CLI's Starter release
267
- pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.30.0 # or a specific tag, branch, or commit
267
+ pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.30.1 # or a specific tag, branch, or commit
268
268
  ```
269
269
 
270
270
  The CLI renders the project twice, from the Starter commit and modules recorded in your `package.json` and from the new state, and merges the difference into your project. The new state has other modules for `add` and `remove`, and the newer Starter with the same modules for `upgrade`. Modules a newer Starter introduces start as `none` where possible, so an upgrade never adds a service on its own.
@@ -413,27 +413,27 @@ Projects created with `--infra neon` also have `.github/workflows/preview.yml`,
413
413
  Each CLI release is tested against one specific Starter release:
414
414
 
415
415
  ```text
416
- create-clubedge-app v0.46.0
416
+ create-clubedge-app v0.46.1
417
417
  |
418
418
  v
419
- Clubedge Starter v0.30.0
420
- commit 4d341955bd13cb170c7385acb745c3d374259897
419
+ Clubedge Starter v0.30.1
420
+ commit 6d04531ffb2cd9f0e76b6e2b3b542f733b66aed6
421
421
  ```
422
422
 
423
423
  The current relationship is:
424
424
 
425
425
  | CLI release | Default Starter release | Meaning |
426
426
  | --- | --- | --- |
427
- | `create-clubedge-app@0.46.0` | `clubedge-starter@v0.30.0` | The CLI's required, tested default |
427
+ | `create-clubedge-app@0.46.1` | `clubedge-starter@v0.30.1` | The CLI's required, tested default |
428
428
 
429
429
  This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
430
430
 
431
- The `v0.30.0` tag is expected to resolve to commit
432
- `4d341955bd13cb170c7385acb745c3d374259897`. The CLI verifies this before
431
+ The `v0.30.1` tag is expected to resolve to commit
432
+ `6d04531ffb2cd9f0e76b6e2b3b542f733b66aed6`. The CLI verifies this before
433
433
  scaffolding with the default reference and fails rather than silently using a
434
434
  moved tag.
435
435
 
436
- When the Starter changes, its maintainers first publish a new versioned release, such as `v0.30.0`. The CLI is then updated to use that release, all generated-project checks must pass, and a new CLI version is published. This keeps Starter and CLI releases independently versioned while preserving a tested compatibility relationship.
436
+ When the Starter changes, its maintainers first publish a new versioned release, such as `v0.30.1`. The CLI is then updated to use that release, all generated-project checks must pass, and a new CLI version is published. This keeps Starter and CLI releases independently versioned while preserving a tested compatibility relationship.
437
437
 
438
438
  Every generated project records its CLI version, Starter repository, Starter ref, Starter commit, framework, and modules in `package.json` and `README.md`.
439
439
 
@@ -488,7 +488,7 @@ CI packs the CLI with `npm pack`, scaffolds clean projects from the packed tarba
488
488
  1. Make and validate changes in `clubedge-starter`.
489
489
  2. Run lint, typecheck, unit tests, browser tests, and production build.
490
490
  3. Update the Starter package versions.
491
- 4. Commit `main`, create a release tag such as `v0.30.0`, and publish the Starter release.
491
+ 4. Commit `main`, create a release tag such as `v0.30.1`, and publish the Starter release.
492
492
 
493
493
  ### CLI maintainers
494
494
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/create-clubedge-app",
3
- "version": "0.46.0",
3
+ "version": "0.46.1",
4
4
  "description": "Create React apps from Clubedge Starter with a CLI or local MCP server.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.9.0",
@@ -24,8 +24,8 @@
24
24
  },
25
25
  "clubedge": {
26
26
  "starterRepository": "Clubedge/clubedge-starter",
27
- "starterRef": "v0.30.0",
28
- "starterCommit": "4d341955bd13cb170c7385acb745c3d374259897"
27
+ "starterRef": "v0.30.1",
28
+ "starterCommit": "6d04531ffb2cd9f0e76b6e2b3b542f733b66aed6"
29
29
  },
30
30
  "license": "Apache-2.0",
31
31
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/api",
3
- "version": "0.30.0",
3
+ "version": "0.30.1",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/api-fastapi",
3
- "version": "0.30.0",
3
+ "version": "0.30.1",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "clubedge-api"
3
- version = "0.30.0"
3
+ version = "0.30.1"
4
4
  description = "The project's API, implementing packages/contract/openapi.yaml."
5
5
  requires-python = ">=3.12"
6
6
  dependencies = [
@@ -389,7 +389,7 @@ wheels = [
389
389
 
390
390
  [[package]]
391
391
  name = "clubedge-api"
392
- version = "0.30.0"
392
+ version = "0.30.1"
393
393
  source = { editable = "." }
394
394
  default-groups = "all"
395
395
  dependencies = [
@@ -6,6 +6,11 @@ import site from "../web/src/config/site.json" with { type: "json" };
6
6
  // reverse-domain id you own, such as com.yourcompany.app, before the first Android or iOS build.
7
7
  const appId = `com.example.${site.serviceId.replace(/[^a-z0-9]/gi, "").replace(/^[0-9]+/, "") || "app"}`;
8
8
 
9
+ // A website on this computer, as in the end-to-end tests (e2e/README.md), may be plain HTTP
10
+ // (scripts/build.mjs): Android then lets the app's https pages call it. A store build's website is
11
+ // https, and keeps Android's refusal.
12
+ const plainHttp = (process.env.WEB_URL ?? "").startsWith("http://");
13
+
9
14
  const config: CapacitorConfig = {
10
15
  appId,
11
16
  appName: site.name,
@@ -15,7 +20,9 @@ const config: CapacitorConfig = {
15
20
  // The app's pages load from https://localhost on Android and capacitor://localhost on iOS:
16
21
  // the origins the website's server takes the app's requests from (MOBILE_ORIGINS).
17
22
  androidScheme: "https",
23
+ ...(plainHttp ? { cleartext: true } : {}),
18
24
  },
25
+ ...(plainHttp ? { android: { allowMixedContent: true } } : {}),
19
26
  // How pnpm release signs the iOS build (scripts/release.mjs), from the environment, never from
20
27
  // this file: Xcode's automatic signing, or, with a provisioning profile named, manual signing as
21
28
  // in CI. The script signs Android's App Bundle itself.
@@ -0,0 +1,23 @@
1
+ # End-to-end tests
2
+
3
+ [Maestro](https://docs.maestro.dev) goes through the app as a user would, against the website and its API: `account.yaml` creates an account, checks the account the API returns, sets a display name, signs out and in, and deletes the account. A step fails when the app can't do what a user would, such as when it signs up but can't load the account.
4
+
5
+ To run it on an Android emulator:
6
+
7
+ 1. Start the website and the API with their database (`pnpm dev`, or `pnpm start` after `pnpm build`), with sign-in that needs no confirmed email.
8
+ 2. Build the app against the website. The emulator reaches your computer at `10.0.2.2`; the iOS simulator at `localhost`. A plain-HTTP `WEB_URL` is allowed only for those addresses (`scripts/build.mjs`):
9
+
10
+ ```sh
11
+ cd apps/shell
12
+ WEB_URL=http://10.0.2.2:3000 pnpm run sync android
13
+ (cd android && ./gradlew assembleDebug)
14
+ adb install -r android/app/build/outputs/apk/debug/app-debug.apk
15
+ ```
16
+
17
+ 3. Run the flow with the app's id, from `capacitor.config.ts` or `android/app/build.gradle`'s `applicationId`:
18
+
19
+ ```sh
20
+ maestro test -e APP_ID=com.example.yourapp e2e
21
+ ```
22
+
23
+ Each run signs up with a new address, `e2e-<time>@example.com`, so a run that stops before deleting its account doesn't block the next one. `-e PASSWORD=...` changes its password.
@@ -0,0 +1,116 @@
1
+ # The account from start to end, in the app's build of the website, against the website and its
2
+ # API: creating it, the account the API returns, a new display name, signing out and in, and
3
+ # deleting it. Each step fails when the app can't do what a user would. e2e/README.md says how
4
+ # to run it. The website's fields are found by their id.
5
+ appId: ${APP_ID}
6
+ name: Account
7
+ env:
8
+ PASSWORD: e2e-password-1234
9
+ NAME: Robin Tester
10
+ ---
11
+ # A new address for each run, so a run that stops before deleting its account doesn't block the
12
+ # next one.
13
+ - evalScript: ${output.email = 'e2e-' + Date.now() + '@example.com'}
14
+ - launchApp:
15
+ clearState: true
16
+ # The app opens on the website's start page; the dashboard asks a signed-out user to sign in.
17
+ - extendedWaitUntil:
18
+ visible: Explore the dashboard.*
19
+ timeout: 60000
20
+ - tapOn: Explore the dashboard.*
21
+ - extendedWaitUntil:
22
+ visible: Create one
23
+ timeout: 30000
24
+ - tapOn: Create one
25
+ - extendedWaitUntil:
26
+ visible: Create your account
27
+ timeout: 15000
28
+ - tapOn:
29
+ id: email
30
+ - inputText: ${output.email}
31
+ - tapOn:
32
+ id: password
33
+ - inputText: ${PASSWORD}
34
+ - hideKeyboard
35
+ - tapOn: Create account
36
+
37
+ # Signed in: the dashboard, and Settings with the account the API returns.
38
+ - extendedWaitUntil:
39
+ visible: A better place to start building.
40
+ timeout: 30000
41
+ - takeScreenshot: 1-dashboard
42
+ # On a phone, the account's links are in the dashboard's menu.
43
+ - tapOn: Open navigation
44
+ - tapOn: Settings
45
+ - extendedWaitUntil:
46
+ visible: Signed in as ${output.email}.
47
+ timeout: 30000
48
+ - takeScreenshot: 2-settings
49
+
50
+ # A display name, which the API keeps.
51
+ - tapOn:
52
+ id: displayName
53
+ - inputText: ${NAME}
54
+ - hideKeyboard
55
+ - tapOn: Save profile
56
+ - extendedWaitUntil:
57
+ visible: Your profile was saved.
58
+ timeout: 15000
59
+
60
+ # Signing out, then in again: the session and the name come back.
61
+ - tapOn: Dashboard
62
+ - extendedWaitUntil:
63
+ visible: Open navigation
64
+ timeout: 15000
65
+ - tapOn: Open navigation
66
+ - tapOn: Sign out
67
+ - extendedWaitUntil:
68
+ visible: Forgot password\?
69
+ timeout: 15000
70
+ - tapOn:
71
+ id: email
72
+ - inputText: ${output.email}
73
+ - tapOn:
74
+ id: password
75
+ - inputText: ${PASSWORD}
76
+ - hideKeyboard
77
+ # The button, under the page's title of the same name.
78
+ - tapOn:
79
+ text: Sign in
80
+ index: 1
81
+ - extendedWaitUntil:
82
+ visible: A better place to start building.
83
+ timeout: 30000
84
+ - tapOn: Open navigation
85
+ - tapOn: Settings
86
+ - extendedWaitUntil:
87
+ visible: Signed in as ${output.email}.
88
+ timeout: 30000
89
+ - assertVisible: ${NAME}
90
+ - takeScreenshot: 3-signed-in-again
91
+
92
+ # Deleting the account signs out, and its password no longer signs in.
93
+ - scrollUntilVisible:
94
+ element: Delete my account
95
+ - tapOn:
96
+ id: password
97
+ - inputText: ${PASSWORD}
98
+ - hideKeyboard
99
+ - tapOn: Delete my account
100
+ - extendedWaitUntil:
101
+ visible: Your account was deleted.
102
+ timeout: 30000
103
+ - takeScreenshot: 4-deleted
104
+ - tapOn:
105
+ id: email
106
+ - inputText: ${output.email}
107
+ - tapOn:
108
+ id: password
109
+ - inputText: ${PASSWORD}
110
+ - hideKeyboard
111
+ - tapOn:
112
+ text: Sign in
113
+ index: 1
114
+ - extendedWaitUntil:
115
+ visible: Email or password could not be verified.
116
+ timeout: 30000
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/capacitor",
3
- "version": "0.30.0",
3
+ "version": "0.30.1",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -21,7 +21,8 @@ const platforms = ["android", "ios"];
21
21
 
22
22
  /**
23
23
  * The website's origin, from WEB_URL. It must be https: the app sends its session there, and
24
- * emailed links open it.
24
+ * emailed links open it. Only a website on this computer may be plain HTTP, as in the end-to-end
25
+ * tests: the Android emulator reaches it at 10.0.2.2, and the iOS simulator at localhost.
25
26
  */
26
27
  export function webOrigin(value) {
27
28
  const example = "such as https://app.example.com";
@@ -34,7 +35,8 @@ export function webOrigin(value) {
34
35
  } catch {
35
36
  throw new Error(`WEB_URL must be the website's address, ${example}, not "${value}".`);
36
37
  }
37
- if (url.protocol !== "https:") {
38
+ const local = ["10.0.2.2", "localhost", "127.0.0.1"].includes(url.hostname);
39
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
38
40
  throw new Error("WEB_URL must be an https:// address: the app sends its session there.");
39
41
  }
40
42
  if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
@@ -1,4 +1,5 @@
1
1
  import type { ConfigContext, ExpoConfig } from "expo/config";
2
+ import { type ConfigPlugin, withAndroidManifest } from "expo/config-plugins";
2
3
  import site from "./src/config/site.json";
3
4
 
4
5
  // The app's id on Android and iOS, made from the project's name until you set your own. It
@@ -33,6 +34,18 @@ export function buildNumber(version: string): number {
33
34
  return major * 1_000_000 + minor * 1_000 + patch;
34
35
  }
35
36
 
37
+ // A build that calls its API over plain HTTP, such as the API on this computer in the end-to-end
38
+ // tests (e2e/README.md), may: Android and iOS refuse plain HTTP otherwise. A store build calls an
39
+ // https address, and keeps that refusal.
40
+ const plainHttp = (process.env.EXPO_PUBLIC_API_URL ?? "").startsWith("http://");
41
+
42
+ const allowPlainHttpOnAndroid: ConfigPlugin = (config) =>
43
+ withAndroidManifest(config, (android) => {
44
+ const application = android.modResults.manifest.application?.[0];
45
+ if (application) application.$["android:usesCleartextTraffic"] = "true";
46
+ return android;
47
+ });
48
+
36
49
  // Firebase's Android settings, which FCM needs to deliver notifications: the path to // clubedge:only push!=none
37
50
  // google-services.json, from .env.local or an EAS file variable of the same name. // clubedge:only push!=none
38
51
  const googleServicesFile = process.env.GOOGLE_SERVICES_JSON || undefined; // clubedge:only push!=none
@@ -44,7 +57,7 @@ export default ({ config }: ConfigContext): ExpoConfig => {
44
57
  // APP_LATEST_VERSION: raise it with each release.
45
58
  const version = config.version ?? "1.0.0";
46
59
  const projectId: unknown = config.extra?.eas?.projectId; // clubedge:only ota=eas
47
- return {
60
+ const app: ExpoConfig = {
48
61
  ...config,
49
62
  name: site.name,
50
63
  slug: site.serviceId,
@@ -58,6 +71,9 @@ export default ({ config }: ConfigContext): ExpoConfig => {
58
71
  bundleIdentifier: appId,
59
72
  buildNumber: String(buildNumber(version)),
60
73
  supportsTablet: true,
74
+ ...(plainHttp
75
+ ? { infoPlist: { NSAppTransportSecurity: { NSAllowsArbitraryLoads: true } } }
76
+ : {}),
61
77
  },
62
78
  android: {
63
79
  package: appId,
@@ -91,4 +107,5 @@ export default ({ config }: ConfigContext): ExpoConfig => {
91
107
  ],
92
108
  ],
93
109
  };
110
+ return plainHttp ? allowPlainHttpOnAndroid(app) : app;
94
111
  };
@@ -0,0 +1,26 @@
1
+ # End-to-end tests
2
+
3
+ [Maestro](https://docs.maestro.dev) goes through the app as a user would, on a release build that calls a real API: `account.yaml` creates an account, checks the account the API returns, sets a display name, signs out and in, and deletes the account. A step fails when the app can't do what a user would, such as when it signs in but can't load the account.
4
+
5
+ To run it on an Android emulator:
6
+
7
+ 1. Start the API with its database (`pnpm dev`, or `pnpm start` after `pnpm build`), with sign-in that needs no confirmed email.
8
+ 2. Build the app against it. The emulator reaches your computer at `10.0.2.2`; the iOS simulator at `localhost`. A plain-HTTP address is allowed only in such a build (`app.config.ts`). Keep `EXPO_PUBLIC_API_URL` set for both commands: the release build bundles the app's JavaScript with it.
9
+
10
+ ```sh
11
+ cd apps/mobile
12
+ export EXPO_PUBLIC_API_URL=http://10.0.2.2:3001
13
+ pnpm exec expo prebuild --platform android
14
+ (cd android && ./gradlew assembleRelease)
15
+ adb install -r android/app/build/outputs/apk/release/app-release.apk
16
+ ```
17
+
18
+ With a website, use its address, port 3000, which forwards the API's routes.
19
+
20
+ 3. Run the flow with the app's id, from `android/app/build.gradle`'s `applicationId`:
21
+
22
+ ```sh
23
+ maestro test -e APP_ID=com.example.yourapp e2e
24
+ ```
25
+
26
+ Each run signs up with a new address, `e2e-<time>@example.com`, so a run that stops before deleting its account doesn't block the next one. `-e PASSWORD=...` changes its password.
@@ -0,0 +1,111 @@
1
+ # The account from start to end, on a build of the app that calls a real API: creating it, the
2
+ # account the API returns, a new display name, signing out and in, and deleting it. Each step
3
+ # fails when the app can't do what a user would. e2e/README.md says how to run it.
4
+ appId: ${APP_ID}
5
+ name: Account
6
+ env:
7
+ PASSWORD: e2e-password-1234
8
+ NAME: Robin Tester
9
+ ---
10
+ # A new address for each run, so a run that stops before deleting its account doesn't block the
11
+ # next one.
12
+ - evalScript: ${output.email = 'e2e-' + Date.now() + '@example.com'}
13
+ - launchApp:
14
+ clearState: true
15
+ # Signed out, the app opens on its welcome screen.
16
+ - extendedWaitUntil:
17
+ visible: Create account
18
+ timeout: 60000
19
+ - tapOn: Create account
20
+ - tapOn: Email
21
+ - inputText: ${output.email}
22
+ - tapOn: Password
23
+ - inputText: ${PASSWORD}
24
+ - hideKeyboard
25
+ - tapOn: Create account
26
+
27
+ # Signed in, Home greets the user the API found for the session.
28
+ - extendedWaitUntil:
29
+ visible: Hey e2e-.*
30
+ timeout: 30000
31
+ - assertNotVisible: The API did not find your session.*
32
+ - takeScreenshot: 1-home
33
+
34
+ # Settings shows the account the API returns.
35
+ - tapOn: Settings
36
+ - extendedWaitUntil:
37
+ visible: ${output.email}
38
+ timeout: 15000
39
+ - takeScreenshot: 2-settings
40
+
41
+ # A display name, which the API keeps.
42
+ - tapOn: Edit
43
+ - extendedWaitUntil:
44
+ visible: ${output.email}
45
+ timeout: 15000
46
+ - tapOn: Display name
47
+ - eraseText: 40
48
+ - inputText: ${NAME}
49
+ - hideKeyboard
50
+ - tapOn: Save
51
+ - extendedWaitUntil:
52
+ visible: Saved.
53
+ timeout: 15000
54
+ - tapOn: Back
55
+ - extendedWaitUntil:
56
+ visible: ${NAME}
57
+ timeout: 15000
58
+ - tapOn: Home
59
+ - extendedWaitUntil:
60
+ visible: Hey Robin
61
+ timeout: 15000
62
+ - takeScreenshot: 3-name
63
+
64
+ # Signing out, then in again: the session and the name come back.
65
+ - tapOn: Settings
66
+ - tapOn: Sign out
67
+ - tapOn: Sign out of ${NAME}
68
+ - extendedWaitUntil:
69
+ visible: Continue with email
70
+ timeout: 15000
71
+ - tapOn: Continue with email
72
+ - tapOn: Email
73
+ - inputText: ${output.email}
74
+ - tapOn: Password
75
+ - inputText: ${PASSWORD}
76
+ - hideKeyboard
77
+ - tapOn: Sign in
78
+ - extendedWaitUntil:
79
+ visible: Hey Robin
80
+ timeout: 30000
81
+ - takeScreenshot: 4-signed-in-again
82
+
83
+ # Deleting the account signs out, and its password no longer signs in.
84
+ - tapOn: Settings
85
+ - tapOn: Delete account
86
+ - tapOn: Password
87
+ - inputText: ${PASSWORD}
88
+ - hideKeyboard
89
+ # The button, under the screen's title of the same name.
90
+ - tapOn:
91
+ text: Delete account
92
+ index: 1
93
+ - extendedWaitUntil:
94
+ visible: Delete your account\?
95
+ timeout: 10000
96
+ # Android may show the dialog's buttons in capitals.
97
+ - tapOn: (?i)delete
98
+ - extendedWaitUntil:
99
+ visible: Continue with email
100
+ timeout: 30000
101
+ - takeScreenshot: 5-deleted
102
+ - tapOn: Continue with email
103
+ - tapOn: Email
104
+ - inputText: ${output.email}
105
+ - tapOn: Password
106
+ - inputText: ${PASSWORD}
107
+ - hideKeyboard
108
+ - tapOn: Sign in
109
+ - extendedWaitUntil:
110
+ visible: .*[Ii]nvalid email or password.*
111
+ timeout: 30000
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/expo",
3
- "version": "0.30.0",
3
+ "version": "0.30.1",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "main": "expo-router/entry",
@@ -20,22 +20,22 @@
20
20
  "@clubedge/contract": "workspace:*",
21
21
  "@expo-google-fonts/inter": "^0.4.2",
22
22
  "better-auth": "^1.7.13",
23
- "expo": "~58.0.19",
23
+ "expo": "~58.0.20",
24
24
  "expo-application": "~58.1.2",
25
25
  "expo-blur": "~58.0.5",
26
26
  "expo-constants": "~58.0.13",
27
- "expo-dev-client": "~58.0.15",
27
+ "expo-dev-client": "~58.0.16",
28
28
  "expo-font": "~58.0.10",
29
29
  "expo-linking": "~58.0.14",
30
30
  "expo-network": "~58.0.4",
31
31
  "expo-notifications": "~58.1.6",
32
- "expo-router": "~58.0.26",
32
+ "expo-router": "~58.0.27",
33
33
  "expo-secure-store": "~58.0.2",
34
34
  "expo-splash-screen": "~58.0.11",
35
35
  "expo-sqlite": "~58.0.14",
36
36
  "expo-status-bar": "~58.0.4",
37
37
  "expo-updates": "~58.0.18",
38
- "expo-web-browser": "~58.0.6",
38
+ "expo-web-browser": "~58.0.7",
39
39
  "phosphor-react-native": "^3.0.6",
40
40
  "react": "19.3.0",
41
41
  "react-native": "0.88.0",
@@ -1,6 +1,7 @@
1
1
  import { createApiClient } from "@clubedge/contract";
2
2
  import { apiUrl } from "@/lib/config";
3
+ import { send } from "@/lib/send";
3
4
 
4
5
  // The API, through the website: its server forwards /api to the API service. // clubedge:only framework!=none
5
6
  // The API, which the app calls at its own address. // clubedge:only framework=none
6
- export const api = createApiClient({ baseUrl: apiUrl });
7
+ export const api = createApiClient({ baseUrl: apiUrl, fetch: (input, init) => send(input, init) });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Sends one of the API client's requests to React Native's fetch as an address and its options,
3
+ * the way Better Auth's client sends its own. `extraHeaders`, such as the session's, are added to
4
+ * the request's own; the app sends no cookies of its own.
5
+ */
6
+ export async function send(
7
+ input: RequestInfo | URL,
8
+ init?: RequestInit,
9
+ extraHeaders: Record<string, string> = {},
10
+ ): Promise<Response> {
11
+ // The API client (openapi-fetch) always hands over a Request.
12
+ const request = input as Request;
13
+ const headers: Record<string, string> = {};
14
+ request.headers.forEach((value, name) => {
15
+ headers[name] = value;
16
+ });
17
+ Object.assign(headers, extraHeaders);
18
+ const body =
19
+ request.method === "GET" || request.method === "HEAD" ? undefined : await request.text();
20
+ return fetch(request.url, {
21
+ ...init,
22
+ method: request.method,
23
+ headers,
24
+ body,
25
+ credentials: "omit",
26
+ });
27
+ }
@@ -1,18 +1,13 @@
1
1
  import { createApiClient } from "@clubedge/contract";
2
2
  import { authClient } from "@/lib/auth";
3
3
  import { apiUrl } from "@/lib/config";
4
+ import { send } from "@/lib/send";
4
5
 
5
6
  // The API, through the website: its server forwards /api to the API service, with the session. // clubedge:only framework!=none
6
7
  // The API, which the app calls at its own address, with the session. // clubedge:only framework=none
7
- export const api = createApiClient({ baseUrl: apiUrl });
8
-
9
- api.use({
10
- async onRequest({ request }) {
11
- // The auth client says how the session travels: a bearer token, or the session cookie it
12
- // keeps on the device. The app sends no cookies of its own.
13
- for (const [name, value] of Object.entries(await authClient.sessionHeaders())) {
14
- request.headers.set(name, value);
15
- }
16
- return new Request(request, { credentials: "omit" });
17
- },
8
+ // The auth client says how the session travels: a bearer token, or the session cookie it keeps
9
+ // on the device.
10
+ export const api = createApiClient({
11
+ baseUrl: apiUrl,
12
+ fetch: async (input, init) => send(input, init, await authClient.sessionHeaders()),
18
13
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/start",
3
- "version": "0.30.0",
3
+ "version": "0.30.1",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -41,8 +41,8 @@
41
41
  "@tanstack/react-router": "1.170.42",
42
42
  "@tanstack/react-start": "1.168.61",
43
43
  "lucide-react": "^0.468.0",
44
- "react": "^19.2.0",
45
- "react-dom": "^19.2.0",
44
+ "react": "^19.3.1",
45
+ "react-dom": "^19.3.1",
46
46
  "use-sync-external-store": "^1.7.0",
47
47
  "zod": "^4.6.7"
48
48
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/vite",
3
- "version": "0.30.0",
3
+ "version": "0.30.1",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -29,8 +29,8 @@
29
29
  "drizzle-orm": "^0.45.4",
30
30
  "firebase": "^12.15.0",
31
31
  "lucide-react": "^0.468.0",
32
- "react": "^19.2.0",
33
- "react-dom": "^19.2.0"
32
+ "react": "^19.3.1",
33
+ "react-dom": "^19.3.1"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@tailwindcss/vite": "^4.3.3",