Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Zaparoo App is a mobile-first React 19 + TypeScript + Vite application for Zapar
- `npm run build:server` — development server build, then Capacitor sync.
- `npm run build:analyze` — analyzer build.
- `npm run sync` — Capacitor sync only.
- `npm run live-update` — signed live update upload; requires `live-update-private.pem`.
- `npm run live-update` — signed live update upload; requires `live-update-private.pem`, `LIVE_UPDATE_CHANNEL`, and `VITE_RELEASE_KEY`.
- `npx cap open ios` / `npx cap open android` — open native projects.

## Project layout
Expand Down Expand Up @@ -66,7 +66,7 @@ docs/ focused guides for Capacitor, deployment, and testing
### Ask first

- Adding a Capacitor plugin, because it forces a native rebuild and store release.
- Bumping the app version; see `docs/deployment.md` because three files must change in lockstep.
- Bumping the app version; see `docs/deployment.md` because package, Android, iOS, What's New, and live update channels must stay in lockstep.
- Refactors that touch many files or shared infrastructure.
- Force-pushes, branch deletes, force-resets, hook bypasses, or anything that overwrites others' work.
- Starting long-running local servers.
Expand Down Expand Up @@ -163,7 +163,7 @@ docs/ focused guides for Capacitor, deployment, and testing

- Keep the summary concise.
- Do not include a separate test plan section.
- For a store release, follow the checklist in `docs/deployment.md` and bump all version fields in lockstep.
- For a store release, follow the checklist in `docs/deployment.md`, keep Android `versionCode` and iOS `CURRENT_PROJECT_VERSION` matched, and keep `src/__tests__/validation/release-config.test.ts` passing.

## Pointer docs

Expand Down
5 changes: 3 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ android {
applicationId "dev.wizzo.tapto"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 26
versionName "1.11.1"
versionCode 27
versionName "1.11.2"
resValue "string", "capawesome_live_update_default_channel", "production-" + defaultConfig.versionCode
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
Expand Down
1 change: 0 additions & 1 deletion capacitor.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ const config: CapacitorConfig = {
LiveUpdate: {
appId: "e96c260c-3271-4895-bff0-de9f8ca4d05a",
autoUpdateStrategy: "background",
defaultChannel: "production",
readyTimeout: 10000,
publicKey:
"-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9VF6x0L6R3Sq8Xk4NdNV6oR+Te7RyiaFaTsbsrzHrzWpk3g6WQrBCZoE2xSmvlTUeroEu4EW2JotftOmklkt7hRWKaxtwuCp6CwtUsBHmK9WWDdygSGqHUlotx+TMnLRKFrXsTVMPdbxfYkP2srsiNnL7CYjjdDn65xAHZ79JcR1k0JrCGgW5xDDsxeT54A8x+obtezMA8iIun1etmDeb03KHSshtkYVPmmBPjOxre9/zkjF12je6ZpsK3Vdt00HmN5sMCXvBVEv5ZKvsGDYpEXPZidIHvA/4So0FHnFT5PsGktYk/PLtirP7frA6v3xhj1Gwlr4ffnGTGHLqe4TJwIDAQAB-----END PUBLIC KEY-----",
Expand Down
62 changes: 39 additions & 23 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This guide covers CI/CD with Capawesome Cloud and over-the-air (OTA) live update

## Live Updates

The app uses Capawesome Cloud for OTA updates, allowing JS/HTML/CSS changes to be pushed directly to users without app store review.
The app uses Capawesome Cloud for OTA updates, allowing binary-compatible JS/HTML/CSS changes to be pushed directly to users without app store review. Do not use Live Updates for native dependency, Capacitor plugin, permission, or plugin configuration changes.

### How It Works

Expand All @@ -20,7 +20,6 @@ Located in `capacitor.config.ts`:
LiveUpdate: {
appId: "your-app-id",
autoUpdateStrategy: "background",
defaultChannel: "production",
readyTimeout: 10000,
publicKey: "-----BEGIN PUBLIC KEY-----...",
}
Expand All @@ -36,18 +35,28 @@ Called in `App.tsx` after successful render:

If a bad update crashes the app before `ready()` is called, the plugin automatically rolls back to the previous working version.

### Versioned Channels

Zaparoo App 1.11.2 and newer use Capawesome versioned channels. Each native build listens on `production-<build-number>`:

- Android sets `capawesome_live_update_default_channel` to `production-` + `versionCode` in `android/app/build.gradle`.
- iOS sets `CapawesomeLiveUpdateDefaultChannel` to `production-$(CURRENT_PROJECT_VERSION)` in `ios/App/App/Info.plist`.

The old shared `production` channel is only for older installed apps. Never deploy 1.11.2+ web bundles to shared `production`.

### Pushing a Live Update

```bash
VITE_RELEASE_KEY="live:1.11.1-ota.1" npm run live-update
LIVE_UPDATE_CHANNEL="production-27" VITE_RELEASE_KEY="live:1.11.2-ota.1" npm run live-update
```

This command:

1. Requires `VITE_RELEASE_KEY` so the app can identify the applied web bundle for the “What’s new” dialog
2. Builds the web assets (`npm run build:web`)
3. Signs the bundle with `live-update-private.pem`
4. Uploads to Capawesome Cloud with `releaseKey` as a custom property
1. Requires `LIVE_UPDATE_CHANNEL` so the bundle goes to the matching native build channel
2. Requires `VITE_RELEASE_KEY` so the app can identify the applied web bundle for the “What’s new” dialog
3. Builds the web assets (`npm run build:web`)
4. Signs the bundle with `live-update-private.pem`
5. Uploads to Capawesome Cloud with `releaseKey` as a custom property

### Listing Deployed Updates

Expand Down Expand Up @@ -87,29 +96,36 @@ Required for:
- `package.json` (`version`)
- `package-lock.json` (top-level and root package `version`)
- `android/app/build.gradle` (`versionCode`, `versionName`)
- `ios/App/App.xcodeproj/project.pbxproj` (`MARKETING_VERSION`; `CURRENT_PROJECT_VERSION` if the iOS build number changes)
3. Confirm `src/lib/whatsNew.ts` has announcement `releaseKeys` for the native version/build, e.g. `native:1.11.1+26` and `native:1.11.1+1`.
4. After Capacitor upgrades, confirm GitHub Actions and Capawesome Cloud use a Node.js version supported by `@capacitor/cli`. Capacitor 8 requires Node 22+.
5. Check About page credits in `src/routes/settings.about.tsx`:
- `ios/App/App.xcodeproj/project.pbxproj` (`MARKETING_VERSION`, `CURRENT_PROJECT_VERSION`)
3. Increment both platform build numbers on every store release. Android `versionCode` and iOS `CURRENT_PROJECT_VERSION` must match so versioned live update channels line up.
4. Confirm `src/lib/whatsNew.ts` has announcement `releaseKeys` for the native version/build, e.g. `native:1.11.2+27`.
5. Confirm the release guard in `src/__tests__/validation/release-config.test.ts` covers the version/channel invariants.
6. After Capacitor upgrades, confirm GitHub Actions and Capawesome Cloud use a Node.js version supported by `@capacitor/cli`. Capacitor 8 requires Node 22+.
7. Check About page credits in `src/routes/settings.about.tsx`:
- translation credits
- active Patreon CSV export names
- Patreon tier coloring (`#F1C40D` Supporter/Sponsor, `#E74C3C` Mega Supporter, `#E91E63` Ultra Supporter)
6. Run validation:
8. Run validation:
- `npm run typecheck`
- `npm run format:check`
- `npm run lint`
- `npm run test -- --run`
7. Optionally run `npm run build:web` for a web-only production build. Run `npm run build` only when ready for Capacitor sync/native file updates.
8. Commit the release prep changes.
9. Create and push the version tag:
9. Run `npm run sync` when native Live Update config changes and inspect generated native config diffs.
10. Optionally run `npm run build:web` for a web-only production build. Run `npm run build` only when ready for Capacitor sync/native file updates.
11. Commit the release prep changes.
12. Create and push the version tag:

```bash
git tag v1.11.2
git push origin v1.11.2
```

13. Confirm GitHub release artifacts and Capawesome Cloud iOS/Android builds complete.
14. Submit/release from App Store Connect and Google Play Console.

```bash
git tag v1.11.1
git push origin v1.11.1
```
### Failed Release Tags

10. Confirm GitHub release artifacts and Capawesome Cloud iOS/Android builds complete.
11. Submit/release from App Store Connect and Google Play Console.
`v1.11.0` and `v1.11.1` are abandoned failed tags. Do not submit or promote builds from those tags.

---

Expand Down Expand Up @@ -146,8 +162,8 @@ The app uses Capawesome Cloud for building iOS and Android binaries.

```bash
# Create and push a version tag
git tag v1.11.1
git push origin v1.11.1
git tag v1.11.2
git push origin v1.11.2
```

### Build Runtime
Expand Down
8 changes: 4 additions & 4 deletions ios/App/App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 27;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = App/Info.plist;
Expand All @@ -382,7 +382,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.11.1;
MARKETING_VERSION = 1.11.2;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = dev.wizzo.tapto;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -402,7 +402,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 27;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = App/Info.plist;
Expand All @@ -413,7 +413,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.11.1;
MARKETING_VERSION = 1.11.2;
PRODUCT_BUNDLE_IDENTIFIER = dev.wizzo.tapto;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
Expand Down
2 changes: 2 additions & 0 deletions ios/App/App/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>CapawesomeLiveUpdateDefaultChannel</key>
<string>production-$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NFCReaderUsageDescription</key>
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "zaparoo-app",
"private": true,
"license": "Apache-2.0",
"version": "1.11.1",
"version": "1.11.2",
"type": "module",
"engines": {
"node": ">=22.0.0"
Expand All @@ -16,7 +16,7 @@
"sync": "npx cap sync",
"build:server": "tsc --noEmit -p tsconfig.build.json && vite build && cross-env NODE_ENV=development cap sync",
"build:analyze": "tsc --noEmit -p tsconfig.build.json && vite build --mode analyze",
"live-update": "sh -c 'test -n \"$VITE_RELEASE_KEY\" || { echo \"VITE_RELEASE_KEY is required for live updates\"; exit 1; }; npm run build:web && npx capawesome apps:liveupdates:upload --app-id e96c260c-3271-4895-bff0-de9f8ca4d05a --path dist --private-key live-update-private.pem --channel production --custom-property releaseKey=$VITE_RELEASE_KEY'",
"live-update": "sh -c 'test -n \"$VITE_RELEASE_KEY\" || { echo \"VITE_RELEASE_KEY is required for live updates\"; exit 1; }; test -n \"$LIVE_UPDATE_CHANNEL\" || { echo \"LIVE_UPDATE_CHANNEL is required for live updates\"; exit 1; }; npm run build:web && npx capawesome apps:liveupdates:upload --app-id e96c260c-3271-4895-bff0-de9f8ca4d05a --path dist --private-key live-update-private.pem --channel \"$LIVE_UPDATE_CHANNEL\" --custom-property releaseKey=$VITE_RELEASE_KEY'",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/unit/lib/whatsNew.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ describe("whatsNew", () => {
expect(identity.liveBundleId).toBe("bundle-2026-06-04");
});

it("should find the first 1.11.1 announcement", () => {
const announcement = getWhatsNewAnnouncement("native:1.11.1+26");
it("should find the first 1.11.2 announcement", () => {
const announcement = getWhatsNewAnnouncement("native:1.11.2+27");

expect(announcement?.id).toBe("release-1.11.1");
expect(announcement?.id).toBe("release-1.11.2");
expect(announcement?.items).toHaveLength(5);
});
});
94 changes: 94 additions & 0 deletions src/__tests__/validation/release-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "fs";
import { resolve } from "path";
import { WHATS_NEW_ANNOUNCEMENTS } from "@/lib/whatsNew";

type PackageJson = {
version: string;
scripts: Record<string, string>;
};

const root = resolve(__dirname, "../../..");

function readProjectFile(path: string): string {
return readFileSync(resolve(root, path), "utf-8");
}

function readPackageJson(): PackageJson {
return JSON.parse(readProjectFile("package.json")) as PackageJson;
}

function requireMatch(source: string, pattern: RegExp, label: string): string {
const match = source.match(pattern);
if (!match?.[1]) {
throw new Error(`Missing ${label}`);
}
return match[1];
}

describe("release configuration", () => {
it("should keep app version surfaces aligned", () => {
const packageJson = readPackageJson();
const androidGradle = readProjectFile("android/app/build.gradle");
const xcodeProject = readProjectFile(
"ios/App/App.xcodeproj/project.pbxproj",
);

const androidVersionName = requireMatch(
androidGradle,
/versionName\s+"([^"]+)"/,
"Android versionName",
);
const androidVersionCode = Number(
requireMatch(androidGradle, /versionCode\s+(\d+)/, "Android versionCode"),
);
const iosMarketingVersions = [
...xcodeProject.matchAll(/MARKETING_VERSION = ([^;]+);/g),
].map((match) => match[1]);
const iosBuildNumbers = [
...xcodeProject.matchAll(/CURRENT_PROJECT_VERSION = (\d+);/g),
].map((match) => Number(match[1]));

expect(androidVersionName).toBe(packageJson.version);
expect(new Set(iosMarketingVersions)).toEqual(
new Set([packageJson.version]),
);
expect(new Set(iosBuildNumbers)).toEqual(new Set([androidVersionCode]));
expect(androidVersionCode).toBeGreaterThan(26);
});

it("should use versioned Capawesome live update channels", () => {
const androidGradle = readProjectFile("android/app/build.gradle");
const iosInfoPlist = readProjectFile("ios/App/App/Info.plist");
const capacitorConfig = readProjectFile("capacitor.config.ts");
const packageJson = readPackageJson();
const liveUpdateScript = packageJson.scripts["live-update"];

expect(androidGradle).toContain(
'resValue "string", "capawesome_live_update_default_channel", "production-" + defaultConfig.versionCode',
);
expect(iosInfoPlist).toContain("CapawesomeLiveUpdateDefaultChannel");
expect(iosInfoPlist).toContain("production-$(CURRENT_PROJECT_VERSION)");
expect(capacitorConfig).not.toContain('defaultChannel: "production"');
expect(liveUpdateScript).toContain("LIVE_UPDATE_CHANNEL");
expect(liveUpdateScript).not.toContain("--channel production");
});

it("should have a What's New release key for the native build", () => {
const packageJson = readPackageJson();
const androidGradle = readProjectFile("android/app/build.gradle");
const androidVersionCode = requireMatch(
androidGradle,
/versionCode\s+(\d+)/,
"Android versionCode",
);

expect(
WHATS_NEW_ANNOUNCEMENTS.some((announcement) =>
announcement.releaseKeys.includes(
`native:${packageJson.version}+${androidVersionCode}`,
),
),
).toBe(true);
});
});
6 changes: 3 additions & 3 deletions src/lib/whatsNew.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ export type WhatsNewAnnouncement = {

export const WHATS_NEW_ANNOUNCEMENTS: WhatsNewAnnouncement[] = [
{
id: "release-1.11.1",
releaseKeys: ["native:1.11.1+26", "native:1.11.1+1"],
title: "What's new in 1.11.1",
id: "release-1.11.2",
releaseKeys: ["native:1.11.2+27"],
title: "What's new in 1.11.2",
items: [
"Use the new Controls modal for remote and keyboard input.",
"View and manage existing mappings from the Mappings page.",
Expand Down
Loading