@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.
- package/CHANGELOG.md +8 -0
- package/README.md +10 -10
- package/package.json +3 -3
- package/template/apps/api/package.json +1 -1
- package/template/apps/api-fastapi/package.json +1 -1
- package/template/apps/api-fastapi/pyproject.toml +1 -1
- package/template/apps/api-fastapi/uv.lock +1 -1
- package/template/apps/capacitor/capacitor.config.ts +7 -0
- package/template/apps/capacitor/e2e/README.md +23 -0
- package/template/apps/capacitor/e2e/account.yaml +116 -0
- package/template/apps/capacitor/package.json +1 -1
- package/template/apps/capacitor/scripts/build.mjs +4 -2
- package/template/apps/expo/app.config.ts +18 -1
- package/template/apps/expo/e2e/README.md +26 -0
- package/template/apps/expo/e2e/account.yaml +111 -0
- package/template/apps/expo/package.json +5 -5
- package/template/apps/expo/src/lib/api.ts +2 -1
- package/template/apps/expo/src/lib/send.ts +27 -0
- package/template/apps/expo/src/variants/api.auth.ts +6 -11
- package/template/apps/start/package.json +3 -3
- package/template/apps/vite/package.json +3 -3
- package/template/apps/web/package.json +3 -3
- package/template/clubedge.template.json +1 -0
- package/template/package.json +1 -1
- package/template/packages/auth/package.json +1 -1
- package/template/packages/auth-appwrite/package.json +1 -1
- package/template/packages/auth-better-auth/package.json +1 -1
- package/template/packages/auth-firebase/package.json +1 -1
- package/template/packages/auth-supabase/package.json +1 -1
- package/template/packages/cache/package.json +1 -1
- package/template/packages/cache-redis/package.json +1 -1
- package/template/packages/contract/package.json +1 -1
- package/template/packages/contract/src/app-update.test.ts +21 -0
- package/template/packages/contract/src/app-update.ts +4 -2
- package/template/packages/contract/src/index.ts +1 -1
- package/template/packages/core/package.json +1 -1
- package/template/packages/db/package.json +1 -1
- package/template/packages/email/package.json +1 -1
- package/template/packages/email-resend/package.json +1 -1
- package/template/packages/email-smtp/package.json +1 -1
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/jobs-bullmq/package.json +1 -1
- package/template/packages/jobs-inngest/package.json +1 -1
- package/template/packages/jobs-pg-boss/package.json +1 -1
- package/template/packages/local-db/package.json +1 -1
- package/template/packages/mobile/package.json +5 -5
- package/template/packages/observability/package.json +1 -1
- package/template/packages/observability-otel/package.json +1 -1
- package/template/packages/observability-sentry/package.json +1 -1
- package/template/packages/payments/package.json +1 -1
- package/template/packages/payments-stripe/package.json +1 -1
- package/template/packages/push/package.json +1 -1
- package/template/packages/push-expo/package.json +1 -1
- package/template/packages/push-native/package.json +1 -1
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage-s3/package.json +1 -1
- package/template/packages/storage-supabase/package.json +1 -1
- package/template/packages/ui/package.json +2 -2
- package/template/pnpm-lock.yaml +374 -376
- 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.
|
|
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.
|
|
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.
|
|
416
|
+
create-clubedge-app v0.46.1
|
|
417
417
|
|
|
|
418
418
|
v
|
|
419
|
-
Clubedge Starter v0.30.
|
|
420
|
-
commit
|
|
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.
|
|
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.
|
|
432
|
-
`
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
28
|
-
"starterCommit": "
|
|
27
|
+
"starterRef": "v0.30.1",
|
|
28
|
+
"starterCommit": "6d04531ffb2cd9f0e76b6e2b3b542f733b66aed6"
|
|
29
29
|
},
|
|
30
30
|
"license": "Apache-2.0",
|
|
31
31
|
"repository": {
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
api
|
|
10
|
-
|
|
11
|
-
|
|
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.
|
|
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.
|
|
45
|
-
"react-dom": "^19.
|
|
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.
|
|
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.
|
|
33
|
-
"react-dom": "^19.
|
|
32
|
+
"react": "^19.3.1",
|
|
33
|
+
"react-dom": "^19.3.1"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@tailwindcss/vite": "^4.3.3",
|