Skip to content

feat(pos-app): add production OTA update support#403

Draft
ignaciosantise wants to merge 19 commits into
mainfrom
feat/ota-updates
Draft

feat(pos-app): add production OTA update support#403
ignaciosantise wants to merge 19 commits into
mainfrom
feat/ota-updates

Conversation

@ignaciosantise

Copy link
Copy Markdown
Collaborator

Summary

This PR adds production OTA update support for dapps/pos-app with Expo Updates, including runtime update checks, update metadata in Settings, and production-only channel configuration.
It also wires release workflows to publish native fingerprint artifacts and adds a dedicated OTA publish workflow that blocks updates when Android or iOS native fingerprints drift.
Documentation was updated in dapps/pos-app/README.md and dapps/pos-app/AGENTS.md to describe publish, rollback, and operational constraints for JS-only OTA releases.
Validation run: npm run lint, npx tsc --noEmit, and npm test -- --watch=false in dapps/pos-app.

OTA Flow

flowchart TD
  A[Release POS workflow] --> B[Expo prebuild with production channel]
  B --> C[Generate Android and iOS native fingerprints]
  C --> D[Upload fingerprint artifacts]
  E[Manual OTA workflow dispatch] --> F[Compute current Android and iOS fingerprints]
  F --> G[Download last production fingerprints]
  G --> H{Fingerprints match?}
  H -- No --> I[Block OTA publish]
  H -- Yes --> J[eas update --channel production]
Loading

@claude

claude Bot commented Mar 3, 2026

Copy link
Copy Markdown

Claude finished @ignaciosantise's task in 3m 20s —— View job


PR Review: feat(pos-app): add production OTA update support

  • Gather context and read changed files
  • Review core implementation files
  • Run subagent checks (license, breaking changes, data classification)
  • Compile final review

Subagent results: ✅ No license issues | ✅ No breaking changes | ✅ No data classification issues

Found 3 issue(s)

Issue 1: Silent immediate reload during active payment transaction

ID: use-ota-updates-silent-reload-a3f1
File: dapps/pos-app/hooks/use-ota-updates.ts:34-42
Severity: HIGH
Category: code_quality

Context:

  • Pattern: Updates.reloadAsync() is called immediately after fetchResult.isNew without any user-facing prompt or transaction-state check. The hook fires on every AppState -> "active" transition (respecting 5-min debounce), including when the user briefly backgrounds the app mid-payment.
  • Risk: A payment flow in progress (scan, QR display, waiting for on-chain confirmation) is silently destroyed mid-transaction when a new OTA bundle is available.
  • Impact: Lost in-flight payment state; customer-facing disruption on a live POS terminal.
  • Trigger: Any OTA update published while a device has the app backgrounded during checkout, then foregrounded.

Recommendation: Check for an active payment route or expose an imperative API that the caller (layout) can use to defer the reload. At minimum, notify the user before reloading:

// e.g., defer until the app returns to idle state
if (fetchResult.isNew) {
  addLog("info", "OTA update ready — will apply on next launch", ...);
  // store a flag and call Updates.reloadAsync() when no transaction is active
}

Fix this →


Issue 2: Third-party GitHub Actions action pinned by tag, not commit SHA

ID: ota-update-pos-unpinned-action-b7c2
File: .github/workflows/ota-update-pos.yaml:52,59
Severity: MEDIUM
Category: security

Context:

  • Pattern: dawidd6/action-download-artifact@v6 is referenced by a mutable tag. Tags can be force-pushed to point to arbitrary commits.
  • Risk: A compromised or updated dawidd6/action-download-artifact could exfiltrate EXPO_TOKEN or POS_ENV_FILE from the runner environment.
  • Impact: Supply chain compromise of the OTA publish step and all credentials it handles.
  • Trigger: Any tag reassignment in the upstream dawidd6/action-download-artifact repository.

Recommendation: Pin to a specific commit SHA:

uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc  # v6.x.x

Fix this →


Issue 3: External domain URL in app configuration

ID: app-json-external-domain-url-e9a4
File: dapps/pos-app/app.json:15
Severity: LOW
Category: security

🔒 External Domain URL (Non-blocking)
URL: https://u.expo.dev/caf3a0d7-e413-45c2-b3b9-879cd30b3501
File: dapps/pos-app/app.json:15

This is the Expo Updates manifest endpoint, required for OTA updates to function. Verify the project ID (caf3a0d7-e413-45c2-b3b9-879cd30b3501) matches the intended EAS project and that the owner: "reown-mobile" field in app.json correctly scopes it to the right organization.

@vercel

vercel Bot commented Mar 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pos-demo Ready Ready Preview, Comment Jun 9, 2026 7:57pm
react-native-examples Ready Ready Preview, Comment Jun 9, 2026 7:57pm

Request Review

…ading

Restore the expo-asset plugin to ensure assets are embedded in native
builds and included in OTA update manifests. Add patch for expo-updates
to load .env files before config evaluation (upstream fix pending in
expo/expo#43635). Defer API URL validation to request time to avoid
crash when env vars load asynchronously. Add expo-channel-name header
for production channel routing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve merge conflicts after Expo 55 upgrade on main. Update
expo-updates to v55.0.12 and regenerate env loading patch for
the new version.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The expo-updates fingerprint:generate command outputs a full JSON object
containing parentheses and special characters. When interpolated directly
into shell scripts via ${{ }}, this caused syntax errors. Fix by piping
through jq to extract just the hash, and using env blocks instead of
inline interpolation in the OTA workflow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rison

Query fingerprints directly from EAS server using eas build:list and
eas fingerprint:compare, eliminating the need to save/download fingerprint
artifacts across workflows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Capture eas CLI output before parsing with jq to gracefully handle
cases where no builds exist yet and the CLI returns non-JSON output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Required for eas CLI commands (build:list, fingerprint:compare) to
identify the project in non-interactive mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EAS fingerprint API only works with EAS Build, but we build natively
with Gradle/Fastlane. Revert to artifact-based approach with the
original fixes: jq hash extraction and env block for safe shell
interpolation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…facts

Replace GitHub Actions artifacts (30-day retention limit) with a
dedicated `fingerprints-dont-remove` branch for storing native build
fingerprints. This ensures fingerprints never expire, so the OTA
safety check always works regardless of time between releases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Called workflows (release-android-base, release-ios-base) now require
contents:write to push fingerprints to the fingerprints-dont-remove
branch. All caller workflows must grant at least that level.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ignaciosantise and others added 5 commits June 9, 2026 15:24
- Add updates.url, runtimeVersion (fingerprint policy), and embedded
  expo-channel-name to app.json so OTA is actually enabled in native
  builds (previously the config plugin wrote ENABLED=false).
- Replace the single fingerprint push with a fetch+rebase+retry loop in
  the Android/iOS release workflows to avoid non-fast-forward failures
  when both jobs push to fingerprints-dont-remove concurrently.
- Drop the dead EAS_UPDATE_CHANNEL env from the prebuild step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	dapps/pos-app/app/payment-failure.tsx
#	dapps/pos-app/app/scan.tsx
#	dapps/pos-app/components/close-button.tsx
#	dapps/pos-app/components/settings-bottom-sheet.tsx
#	dapps/pos-app/package-lock.json
#	dapps/pos-app/package.json
…ompat

expo-updates@55.0.12 declares expo-updates-interface "~55.1.3", which
resolves to 55.1.6. 55.1.4+ added an abstract `getContext()` member to
the UpdatesStateChangeSubscription interface that 55.0.12 does not
implement, breaking :expo-updates:compileReleaseKotlin. Pin the
interface to 55.1.3 (the SDK 55 tested combo) via overrides.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bumps expo 55.0.4 -> 55.0.26 and runs `expo install --fix`, aligning
all expo-* and react-native packages to the SDK-pinned versions
(react-native 0.83.6, expo-updates 55.0.24, etc.).

This replaces the earlier expo-updates-interface@55.1.3 override: the
newer expo-updates 55.0.24 implements the UpdatesStateChangeSubscription
`getContext()` member, so it is natively compatible with
expo-updates-interface 55.1.6 and the override is no longer needed.

Also:
- Regenerate the @expo/env-load patch for expo-updates 55.0.24
- Add babel-preset-expo as a direct dep so it hoists (Metro resolves it
  from the project root; it was only nested under expo/node_modules)
- Bump eslint-config-expo to 55.0.1 to satisfy expo-doctor

Verified: `expo prebuild --clean` regenerates MainApplication.kt with the
new ExpoReactHostFactory API, and a full release `assembleRelease`
succeeds with OTA config embedded (updates URL, fingerprint runtime
version, production channel).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EAS Update requires the --environment flag on Expo SDK 55+. Add
`--environment production` to the OTA workflow's eas update call and the
README CLI example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant