From 14807cd034bd0208111062f0be8fc9b150c049e1 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 20:29:55 +0800 Subject: [PATCH 1/6] feat(kit): extract @clip-wallet/desktop-kit and @clip-wallet/mobile-kit; clip.config languages, app ids, scheme The desktop and phone apps become libraries driven by clip.config.ts, the way the extension became @clip-wallet/extension-kit: apps/desktop and apps/mobile now only hold Clip Wallet's brand (clip.config.ts, icons) and one-line entrypoints. - packages/desktop-kit: the main process, preloads, renderer pages, native-messaging host and their unit tests (moved from apps/desktop). clipDesktop() builds the electron-vite config (resolved config as virtual:clip-wallet/config, icon inlined as the dapp-facing identity, pages' title and CSP, native host bundled after the main build, mainnet checklist); electronBuilderConfig() maps the config to electron-builder (app id, product/executable/artifact names, deep-link scheme, Chromium locales for the offered languages, icons, env-driven signing and update publishing). The app id, scheme, TON Connect key, window title, user-agent scrub and WalletConnect metadata come from the config. - packages/mobile-kit: the screens, background host, in-app browser, plugins and their vitest + jest-expo suites (moved from apps/mobile). expoConfig() maps the config to app.config (name, slug, scheme, bundle id, package, icons, adaptive icon and splash on the accent colour, permission texts); withClipWallet() wires Metro (the resolved config as a generated virtual module, Node shims, one React). Keychain services are .vault / .devicekey (unchanged for Clip Wallet); the onboarding icon is passed in by the app; Ledger BLE loads through import(). - @clip-wallet/config: languages, appId, scheme, desktop/mobile id overrides, fees/usage reserved for Clip Cloud (off, never acted on); platformIds(); @clip-wallet/config/node (loadClipConfigSync, buildEnv, resolveBuildConfig, iconDataUri, openChecklistItems). - @clip-wallet/i18n + ui + both kits: only the languages clip.config offers are matched and listed. - Harness allow-list, brand renderer and tests, CI workflows follow the move. Verified: pnpm -r typecheck, pnpm -r test, pnpm harness; apps/desktop build + both Playwright e2e specs (2/2); apps/mobile expo export --platform ios (28 MB Hermes bundle). Co-Authored-By: Claude Opus 5.5 --- .changeset/desktop-mobile-kits.md | 18 + .github/workflows/ci.yml | 6 +- .github/workflows/codeql.yml | 1 - .github/workflows/desktop.yml | 5 +- apps/desktop/.gitignore | 2 - apps/desktop/electron-builder.config.cjs | 98 +- apps/desktop/electron.vite.config.ts | 77 +- apps/desktop/package.json | 39 +- apps/desktop/scripts/build-native-host.mjs | 21 - apps/desktop/src/main/index.ts | 674 +------- apps/desktop/src/preload/dapp.ts | 46 +- apps/desktop/src/preload/wallet.ts | 41 +- apps/desktop/src/renderer/approval/index.html | 2 +- apps/desktop/src/renderer/approval/main.tsx | 2 +- apps/desktop/src/renderer/browser/main.tsx | 13 +- apps/desktop/src/renderer/public/icon.svg | 5 - apps/desktop/src/renderer/wallet/index.html | 4 +- apps/desktop/src/renderer/wallet/main.tsx | 2 +- apps/desktop/src/shared/app-config.ts | 29 - apps/desktop/tsconfig.json | 2 +- apps/desktop/vitest.config.ts | 5 - apps/mobile/.gitignore | 4 - apps/mobile/app.config.ts | 97 +- apps/mobile/index.ts | 14 +- apps/mobile/metro.config.js | 47 +- apps/mobile/package.json | 51 +- apps/mobile/src/env.ts | 34 - apps/mobile/tsconfig.json | 11 +- apps/mobile/vitest.config.ts | 6 - packages/config/package.json | 11 + packages/config/src/index.ts | 129 ++ packages/config/src/node.ts | 123 ++ packages/config/test/config.test.ts | 97 ++ packages/desktop-kit/.gitignore | 3 + packages/desktop-kit/LICENSE | 202 +++ packages/desktop-kit/NOTICE | 8 + packages/desktop-kit/README.md | 65 + packages/desktop-kit/package.json | 203 +++ .../desktop-kit}/scripts/build-inpage.mjs | 0 packages/desktop-kit/src/builder.ts | 138 ++ packages/desktop-kit/src/csp.ts | 23 + packages/desktop-kit/src/electron-vite.ts | 210 +++ packages/desktop-kit/src/index.ts | 17 + .../desktop-kit}/src/main/app-paths.ts | 20 +- .../desktop-kit}/src/main/app-protocol.ts | 0 .../desktop-kit}/src/main/biometric.ts | 0 .../src/main/browser/browser-ua.ts | 11 + .../desktop-kit}/src/main/browser/browser.ts | 7 +- .../src/main/browser/onemask-relay.ts | 0 .../src/main/browser/url-policy.ts | 0 .../desktop-kit}/src/main/deeplink.ts | 0 .../desktop-kit}/src/main/hid.ts | 0 .../desktop-kit}/src/main/host/link.ts | 0 .../desktop-kit}/src/main/host/wallet.ts | 0 packages/desktop-kit/src/main/index.ts | 673 ++++++++ .../desktop-kit}/src/main/ipc-guard.ts | 0 .../desktop-kit}/src/main/native-hosts.ts | 0 .../desktop-kit}/src/main/storage.ts | 0 .../desktop-kit}/src/main/updater.ts | 0 .../desktop-kit}/src/native-host/main.ts | 0 packages/desktop-kit/src/preload/dapp.ts | 45 + .../desktop-kit}/src/preload/inpage-entry.ts | 0 packages/desktop-kit/src/preload/wallet.ts | 40 + .../src/renderer/browser/BrowserChrome.tsx | 0 .../src/renderer/browser/browser.css | 0 .../src/renderer/browser/mount.tsx | 15 + packages/desktop-kit/src/renderer/index.ts | 8 + .../src/renderer/shared/bridge.ts | 0 .../src/renderer/shared/clients.ts | 0 .../src/renderer/shared/empty-module.ts | 0 .../src/renderer/shared/globals.ts | 0 .../src/renderer/shared/hid-agent.ts | 0 .../src/renderer/shared/mount.tsx | 3 +- packages/desktop-kit/src/shared/app-config.ts | 33 + .../desktop-kit}/src/shared/i18n/ar.ts | 0 .../desktop-kit}/src/shared/i18n/de.ts | 0 .../desktop-kit}/src/shared/i18n/en.ts | 0 .../desktop-kit}/src/shared/i18n/es.ts | 0 .../desktop-kit}/src/shared/i18n/fr.ts | 0 .../desktop-kit}/src/shared/i18n/hi.ts | 0 .../desktop-kit}/src/shared/i18n/index.ts | 4 +- .../desktop-kit}/src/shared/i18n/it.ts | 0 .../desktop-kit}/src/shared/i18n/ja.ts | 0 .../desktop-kit}/src/shared/i18n/ko.ts | 0 .../desktop-kit}/src/shared/i18n/pt-BR.ts | 0 .../desktop-kit}/src/shared/i18n/tr.ts | 0 .../desktop-kit}/src/shared/i18n/zh-Hans.ts | 0 .../desktop-kit}/src/shared/ipc.ts | 0 packages/desktop-kit/test/builder.test.ts | 87 + packages/desktop-kit/test/clip.config.ts | 5 + .../desktop-kit}/test/i18n.test.ts | 0 .../desktop-kit}/test/ipc.test.ts | 0 .../desktop-kit}/test/native-hosts.test.ts | 0 .../desktop-kit}/test/onemask-relay.test.ts | 0 .../desktop-kit}/test/storage.test.ts | 0 .../desktop-kit}/test/url-policy.test.ts | 0 packages/desktop-kit/tsconfig.json | 10 + packages/desktop-kit/vitest.config.ts | 8 + packages/i18n/src/locales.ts | 30 +- packages/i18n/test/i18n.test.tsx | 10 + packages/mobile-kit/.gitignore | 5 + packages/mobile-kit/LICENSE | 202 +++ packages/mobile-kit/NOTICE | 8 + packages/mobile-kit/README.md | 59 + packages/mobile-kit/babel.config.cjs | 4 + .../mobile-kit/jest.config.cjs | 4 +- packages/mobile-kit/package.json | 200 +++ .../mobile-kit}/scripts/build-inpage.mjs | 0 .../scripts/build-plugin-sandbox.mjs | 0 .../mobile-kit}/src/App.tsx | 0 .../mobile-kit}/src/background/argon2.ts | 0 .../src/background/background-task.ts | 0 .../mobile-kit}/src/background/device-key.ts | 10 +- .../mobile-kit}/src/background/events.ts | 0 .../mobile-kit}/src/background/host.ts | 0 .../mobile-kit}/src/background/ledger-ble.ts | 59 +- .../mobile-kit}/src/background/link.ts | 0 .../src/background/notifications.ts | 0 .../mobile-kit}/src/background/passkey.ts | 0 .../mobile-kit}/src/background/storage.ts | 4 +- packages/mobile-kit/src/brand.ts | 16 + .../mobile-kit}/src/browser/bridge.ts | 0 .../mobile-kit}/src/browser/inpage-entry.ts | 0 packages/mobile-kit/src/env.ts | 41 + packages/mobile-kit/src/expo.ts | 151 ++ .../mobile-kit}/src/i18n/ar/accounts.ts | 0 .../mobile-kit}/src/i18n/ar/activity.ts | 0 .../mobile-kit}/src/i18n/ar/app.ts | 0 .../mobile-kit}/src/i18n/ar/approval.ts | 0 .../mobile-kit}/src/i18n/ar/backup.ts | 0 .../mobile-kit}/src/i18n/ar/browser.ts | 0 .../mobile-kit}/src/i18n/ar/buy.ts | 0 .../mobile-kit}/src/i18n/ar/collectibles.ts | 0 .../mobile-kit}/src/i18n/ar/common.ts | 0 .../mobile-kit}/src/i18n/ar/explore.ts | 0 .../mobile-kit}/src/i18n/ar/hardware.ts | 0 .../mobile-kit}/src/i18n/ar/home.ts | 0 .../mobile-kit}/src/i18n/ar/index.ts | 0 .../mobile-kit}/src/i18n/ar/kit.ts | 0 .../mobile-kit}/src/i18n/ar/link.ts | 0 .../mobile-kit}/src/i18n/ar/onboarding.ts | 0 .../mobile-kit}/src/i18n/ar/plugins.ts | 0 .../mobile-kit}/src/i18n/ar/receive.ts | 0 .../mobile-kit}/src/i18n/ar/scan.ts | 0 .../mobile-kit}/src/i18n/ar/security.ts | 0 .../mobile-kit}/src/i18n/ar/send.ts | 0 .../mobile-kit}/src/i18n/ar/settings.ts | 0 .../mobile-kit}/src/i18n/ar/settle.ts | 0 .../mobile-kit}/src/i18n/ar/social.ts | 0 .../mobile-kit}/src/i18n/ar/stake.ts | 0 .../mobile-kit}/src/i18n/ar/swap.ts | 0 .../mobile-kit}/src/i18n/ar/trade.ts | 0 .../mobile-kit}/src/i18n/de/accounts.ts | 0 .../mobile-kit}/src/i18n/de/activity.ts | 0 .../mobile-kit}/src/i18n/de/app.ts | 0 .../mobile-kit}/src/i18n/de/approval.ts | 0 .../mobile-kit}/src/i18n/de/backup.ts | 0 .../mobile-kit}/src/i18n/de/browser.ts | 0 .../mobile-kit}/src/i18n/de/buy.ts | 0 .../mobile-kit}/src/i18n/de/collectibles.ts | 0 .../mobile-kit}/src/i18n/de/common.ts | 0 .../mobile-kit}/src/i18n/de/explore.ts | 0 .../mobile-kit}/src/i18n/de/hardware.ts | 0 .../mobile-kit}/src/i18n/de/home.ts | 0 .../mobile-kit}/src/i18n/de/index.ts | 0 .../mobile-kit}/src/i18n/de/kit.ts | 0 .../mobile-kit}/src/i18n/de/link.ts | 0 .../mobile-kit}/src/i18n/de/onboarding.ts | 0 .../mobile-kit}/src/i18n/de/plugins.ts | 0 .../mobile-kit}/src/i18n/de/receive.ts | 0 .../mobile-kit}/src/i18n/de/scan.ts | 0 .../mobile-kit}/src/i18n/de/security.ts | 0 .../mobile-kit}/src/i18n/de/send.ts | 0 .../mobile-kit}/src/i18n/de/settings.ts | 0 .../mobile-kit}/src/i18n/de/settle.ts | 0 .../mobile-kit}/src/i18n/de/social.ts | 0 .../mobile-kit}/src/i18n/de/stake.ts | 0 .../mobile-kit}/src/i18n/de/swap.ts | 0 .../mobile-kit}/src/i18n/de/trade.ts | 0 .../mobile-kit}/src/i18n/device.ts | 0 .../mobile-kit}/src/i18n/en/accounts.ts | 0 .../mobile-kit}/src/i18n/en/activity.ts | 0 .../mobile-kit}/src/i18n/en/app.ts | 0 .../mobile-kit}/src/i18n/en/approval.ts | 0 .../mobile-kit}/src/i18n/en/backup.ts | 0 .../mobile-kit}/src/i18n/en/browser.ts | 0 .../mobile-kit}/src/i18n/en/buy.ts | 0 .../mobile-kit}/src/i18n/en/collectibles.ts | 0 .../mobile-kit}/src/i18n/en/common.ts | 0 .../mobile-kit}/src/i18n/en/explore.ts | 0 .../mobile-kit}/src/i18n/en/hardware.ts | 0 .../mobile-kit}/src/i18n/en/home.ts | 0 .../mobile-kit}/src/i18n/en/index.ts | 0 .../mobile-kit}/src/i18n/en/kit.ts | 0 .../mobile-kit}/src/i18n/en/link.ts | 0 .../mobile-kit}/src/i18n/en/onboarding.ts | 0 .../mobile-kit}/src/i18n/en/plugins.ts | 0 .../mobile-kit}/src/i18n/en/receive.ts | 0 .../mobile-kit}/src/i18n/en/scan.ts | 0 .../mobile-kit}/src/i18n/en/security.ts | 0 .../mobile-kit}/src/i18n/en/send.ts | 0 .../mobile-kit}/src/i18n/en/settings.ts | 0 .../mobile-kit}/src/i18n/en/settle.ts | 0 .../mobile-kit}/src/i18n/en/social.ts | 0 .../mobile-kit}/src/i18n/en/stake.ts | 0 .../mobile-kit}/src/i18n/en/swap.ts | 0 .../mobile-kit}/src/i18n/en/trade.ts | 0 .../mobile-kit}/src/i18n/es/accounts.ts | 0 .../mobile-kit}/src/i18n/es/activity.ts | 0 .../mobile-kit}/src/i18n/es/app.ts | 0 .../mobile-kit}/src/i18n/es/approval.ts | 0 .../mobile-kit}/src/i18n/es/backup.ts | 0 .../mobile-kit}/src/i18n/es/browser.ts | 0 .../mobile-kit}/src/i18n/es/buy.ts | 0 .../mobile-kit}/src/i18n/es/collectibles.ts | 0 .../mobile-kit}/src/i18n/es/common.ts | 0 .../mobile-kit}/src/i18n/es/explore.ts | 0 .../mobile-kit}/src/i18n/es/hardware.ts | 0 .../mobile-kit}/src/i18n/es/home.ts | 0 .../mobile-kit}/src/i18n/es/index.ts | 0 .../mobile-kit}/src/i18n/es/kit.ts | 0 .../mobile-kit}/src/i18n/es/link.ts | 0 .../mobile-kit}/src/i18n/es/onboarding.ts | 0 .../mobile-kit}/src/i18n/es/plugins.ts | 0 .../mobile-kit}/src/i18n/es/receive.ts | 0 .../mobile-kit}/src/i18n/es/scan.ts | 0 .../mobile-kit}/src/i18n/es/security.ts | 0 .../mobile-kit}/src/i18n/es/send.ts | 0 .../mobile-kit}/src/i18n/es/settings.ts | 0 .../mobile-kit}/src/i18n/es/settle.ts | 0 .../mobile-kit}/src/i18n/es/social.ts | 0 .../mobile-kit}/src/i18n/es/stake.ts | 0 .../mobile-kit}/src/i18n/es/swap.ts | 0 .../mobile-kit}/src/i18n/es/trade.ts | 0 .../mobile-kit}/src/i18n/fr/accounts.ts | 0 .../mobile-kit}/src/i18n/fr/activity.ts | 0 .../mobile-kit}/src/i18n/fr/app.ts | 0 .../mobile-kit}/src/i18n/fr/approval.ts | 0 .../mobile-kit}/src/i18n/fr/backup.ts | 0 .../mobile-kit}/src/i18n/fr/browser.ts | 0 .../mobile-kit}/src/i18n/fr/buy.ts | 0 .../mobile-kit}/src/i18n/fr/collectibles.ts | 0 .../mobile-kit}/src/i18n/fr/common.ts | 0 .../mobile-kit}/src/i18n/fr/explore.ts | 0 .../mobile-kit}/src/i18n/fr/hardware.ts | 0 .../mobile-kit}/src/i18n/fr/home.ts | 0 .../mobile-kit}/src/i18n/fr/index.ts | 0 .../mobile-kit}/src/i18n/fr/kit.ts | 0 .../mobile-kit}/src/i18n/fr/link.ts | 0 .../mobile-kit}/src/i18n/fr/onboarding.ts | 0 .../mobile-kit}/src/i18n/fr/plugins.ts | 0 .../mobile-kit}/src/i18n/fr/receive.ts | 0 .../mobile-kit}/src/i18n/fr/scan.ts | 0 .../mobile-kit}/src/i18n/fr/security.ts | 0 .../mobile-kit}/src/i18n/fr/send.ts | 0 .../mobile-kit}/src/i18n/fr/settings.ts | 0 .../mobile-kit}/src/i18n/fr/settle.ts | 0 .../mobile-kit}/src/i18n/fr/social.ts | 0 .../mobile-kit}/src/i18n/fr/stake.ts | 0 .../mobile-kit}/src/i18n/fr/swap.ts | 0 .../mobile-kit}/src/i18n/fr/trade.ts | 0 .../mobile-kit}/src/i18n/hi/accounts.ts | 0 .../mobile-kit}/src/i18n/hi/activity.ts | 0 .../mobile-kit}/src/i18n/hi/app.ts | 0 .../mobile-kit}/src/i18n/hi/approval.ts | 0 .../mobile-kit}/src/i18n/hi/backup.ts | 0 .../mobile-kit}/src/i18n/hi/browser.ts | 0 .../mobile-kit}/src/i18n/hi/buy.ts | 0 .../mobile-kit}/src/i18n/hi/collectibles.ts | 0 .../mobile-kit}/src/i18n/hi/common.ts | 0 .../mobile-kit}/src/i18n/hi/explore.ts | 0 .../mobile-kit}/src/i18n/hi/hardware.ts | 0 .../mobile-kit}/src/i18n/hi/home.ts | 0 .../mobile-kit}/src/i18n/hi/index.ts | 0 .../mobile-kit}/src/i18n/hi/kit.ts | 0 .../mobile-kit}/src/i18n/hi/link.ts | 0 .../mobile-kit}/src/i18n/hi/onboarding.ts | 0 .../mobile-kit}/src/i18n/hi/plugins.ts | 0 .../mobile-kit}/src/i18n/hi/receive.ts | 0 .../mobile-kit}/src/i18n/hi/scan.ts | 0 .../mobile-kit}/src/i18n/hi/security.ts | 0 .../mobile-kit}/src/i18n/hi/send.ts | 0 .../mobile-kit}/src/i18n/hi/settings.ts | 0 .../mobile-kit}/src/i18n/hi/settle.ts | 0 .../mobile-kit}/src/i18n/hi/social.ts | 0 .../mobile-kit}/src/i18n/hi/stake.ts | 0 .../mobile-kit}/src/i18n/hi/swap.ts | 0 .../mobile-kit}/src/i18n/hi/trade.ts | 0 .../mobile-kit}/src/i18n/index.ts | 0 .../mobile-kit}/src/i18n/it/accounts.ts | 0 .../mobile-kit}/src/i18n/it/activity.ts | 0 .../mobile-kit}/src/i18n/it/app.ts | 0 .../mobile-kit}/src/i18n/it/approval.ts | 0 .../mobile-kit}/src/i18n/it/backup.ts | 0 .../mobile-kit}/src/i18n/it/browser.ts | 0 .../mobile-kit}/src/i18n/it/buy.ts | 0 .../mobile-kit}/src/i18n/it/collectibles.ts | 0 .../mobile-kit}/src/i18n/it/common.ts | 0 .../mobile-kit}/src/i18n/it/explore.ts | 0 .../mobile-kit}/src/i18n/it/hardware.ts | 0 .../mobile-kit}/src/i18n/it/home.ts | 0 .../mobile-kit}/src/i18n/it/index.ts | 0 .../mobile-kit}/src/i18n/it/kit.ts | 0 .../mobile-kit}/src/i18n/it/link.ts | 0 .../mobile-kit}/src/i18n/it/onboarding.ts | 0 .../mobile-kit}/src/i18n/it/plugins.ts | 0 .../mobile-kit}/src/i18n/it/receive.ts | 0 .../mobile-kit}/src/i18n/it/scan.ts | 0 .../mobile-kit}/src/i18n/it/security.ts | 0 .../mobile-kit}/src/i18n/it/send.ts | 0 .../mobile-kit}/src/i18n/it/settings.ts | 0 .../mobile-kit}/src/i18n/it/settle.ts | 0 .../mobile-kit}/src/i18n/it/social.ts | 0 .../mobile-kit}/src/i18n/it/stake.ts | 0 .../mobile-kit}/src/i18n/it/swap.ts | 0 .../mobile-kit}/src/i18n/it/trade.ts | 0 .../mobile-kit}/src/i18n/ja/accounts.ts | 0 .../mobile-kit}/src/i18n/ja/activity.ts | 0 .../mobile-kit}/src/i18n/ja/app.ts | 0 .../mobile-kit}/src/i18n/ja/approval.ts | 0 .../mobile-kit}/src/i18n/ja/backup.ts | 0 .../mobile-kit}/src/i18n/ja/browser.ts | 0 .../mobile-kit}/src/i18n/ja/buy.ts | 0 .../mobile-kit}/src/i18n/ja/collectibles.ts | 0 .../mobile-kit}/src/i18n/ja/common.ts | 0 .../mobile-kit}/src/i18n/ja/explore.ts | 0 .../mobile-kit}/src/i18n/ja/hardware.ts | 0 .../mobile-kit}/src/i18n/ja/home.ts | 0 .../mobile-kit}/src/i18n/ja/index.ts | 0 .../mobile-kit}/src/i18n/ja/kit.ts | 0 .../mobile-kit}/src/i18n/ja/link.ts | 0 .../mobile-kit}/src/i18n/ja/onboarding.ts | 0 .../mobile-kit}/src/i18n/ja/plugins.ts | 0 .../mobile-kit}/src/i18n/ja/receive.ts | 0 .../mobile-kit}/src/i18n/ja/scan.ts | 0 .../mobile-kit}/src/i18n/ja/security.ts | 0 .../mobile-kit}/src/i18n/ja/send.ts | 0 .../mobile-kit}/src/i18n/ja/settings.ts | 0 .../mobile-kit}/src/i18n/ja/settle.ts | 0 .../mobile-kit}/src/i18n/ja/social.ts | 0 .../mobile-kit}/src/i18n/ja/stake.ts | 0 .../mobile-kit}/src/i18n/ja/swap.ts | 0 .../mobile-kit}/src/i18n/ja/trade.ts | 0 .../mobile-kit}/src/i18n/ko/accounts.ts | 0 .../mobile-kit}/src/i18n/ko/activity.ts | 0 .../mobile-kit}/src/i18n/ko/app.ts | 0 .../mobile-kit}/src/i18n/ko/approval.ts | 0 .../mobile-kit}/src/i18n/ko/backup.ts | 0 .../mobile-kit}/src/i18n/ko/browser.ts | 0 .../mobile-kit}/src/i18n/ko/buy.ts | 0 .../mobile-kit}/src/i18n/ko/collectibles.ts | 0 .../mobile-kit}/src/i18n/ko/common.ts | 0 .../mobile-kit}/src/i18n/ko/explore.ts | 0 .../mobile-kit}/src/i18n/ko/hardware.ts | 0 .../mobile-kit}/src/i18n/ko/home.ts | 0 .../mobile-kit}/src/i18n/ko/index.ts | 0 .../mobile-kit}/src/i18n/ko/kit.ts | 0 .../mobile-kit}/src/i18n/ko/link.ts | 0 .../mobile-kit}/src/i18n/ko/onboarding.ts | 0 .../mobile-kit}/src/i18n/ko/plugins.ts | 0 .../mobile-kit}/src/i18n/ko/receive.ts | 0 .../mobile-kit}/src/i18n/ko/scan.ts | 0 .../mobile-kit}/src/i18n/ko/security.ts | 0 .../mobile-kit}/src/i18n/ko/send.ts | 0 .../mobile-kit}/src/i18n/ko/settings.ts | 0 .../mobile-kit}/src/i18n/ko/settle.ts | 0 .../mobile-kit}/src/i18n/ko/social.ts | 0 .../mobile-kit}/src/i18n/ko/stake.ts | 0 .../mobile-kit}/src/i18n/ko/swap.ts | 0 .../mobile-kit}/src/i18n/ko/trade.ts | 0 .../mobile-kit}/src/i18n/pt-BR/accounts.ts | 0 .../mobile-kit}/src/i18n/pt-BR/activity.ts | 0 .../mobile-kit}/src/i18n/pt-BR/app.ts | 0 .../mobile-kit}/src/i18n/pt-BR/approval.ts | 0 .../mobile-kit}/src/i18n/pt-BR/backup.ts | 0 .../mobile-kit}/src/i18n/pt-BR/browser.ts | 0 .../mobile-kit}/src/i18n/pt-BR/buy.ts | 0 .../src/i18n/pt-BR/collectibles.ts | 0 .../mobile-kit}/src/i18n/pt-BR/common.ts | 0 .../mobile-kit}/src/i18n/pt-BR/explore.ts | 0 .../mobile-kit}/src/i18n/pt-BR/hardware.ts | 0 .../mobile-kit}/src/i18n/pt-BR/home.ts | 0 .../mobile-kit}/src/i18n/pt-BR/index.ts | 0 .../mobile-kit}/src/i18n/pt-BR/kit.ts | 0 .../mobile-kit}/src/i18n/pt-BR/link.ts | 0 .../mobile-kit}/src/i18n/pt-BR/onboarding.ts | 0 .../mobile-kit}/src/i18n/pt-BR/plugins.ts | 0 .../mobile-kit}/src/i18n/pt-BR/receive.ts | 0 .../mobile-kit}/src/i18n/pt-BR/scan.ts | 0 .../mobile-kit}/src/i18n/pt-BR/security.ts | 0 .../mobile-kit}/src/i18n/pt-BR/send.ts | 0 .../mobile-kit}/src/i18n/pt-BR/settings.ts | 0 .../mobile-kit}/src/i18n/pt-BR/settle.ts | 0 .../mobile-kit}/src/i18n/pt-BR/social.ts | 0 .../mobile-kit}/src/i18n/pt-BR/stake.ts | 0 .../mobile-kit}/src/i18n/pt-BR/swap.ts | 0 .../mobile-kit}/src/i18n/pt-BR/trade.ts | 0 .../mobile-kit}/src/i18n/tr/accounts.ts | 0 .../mobile-kit}/src/i18n/tr/activity.ts | 0 .../mobile-kit}/src/i18n/tr/app.ts | 0 .../mobile-kit}/src/i18n/tr/approval.ts | 0 .../mobile-kit}/src/i18n/tr/backup.ts | 0 .../mobile-kit}/src/i18n/tr/browser.ts | 0 .../mobile-kit}/src/i18n/tr/buy.ts | 0 .../mobile-kit}/src/i18n/tr/collectibles.ts | 0 .../mobile-kit}/src/i18n/tr/common.ts | 0 .../mobile-kit}/src/i18n/tr/explore.ts | 0 .../mobile-kit}/src/i18n/tr/hardware.ts | 0 .../mobile-kit}/src/i18n/tr/home.ts | 0 .../mobile-kit}/src/i18n/tr/index.ts | 0 .../mobile-kit}/src/i18n/tr/kit.ts | 0 .../mobile-kit}/src/i18n/tr/link.ts | 0 .../mobile-kit}/src/i18n/tr/onboarding.ts | 0 .../mobile-kit}/src/i18n/tr/plugins.ts | 0 .../mobile-kit}/src/i18n/tr/receive.ts | 0 .../mobile-kit}/src/i18n/tr/scan.ts | 0 .../mobile-kit}/src/i18n/tr/security.ts | 0 .../mobile-kit}/src/i18n/tr/send.ts | 0 .../mobile-kit}/src/i18n/tr/settings.ts | 0 .../mobile-kit}/src/i18n/tr/settle.ts | 0 .../mobile-kit}/src/i18n/tr/social.ts | 0 .../mobile-kit}/src/i18n/tr/stake.ts | 0 .../mobile-kit}/src/i18n/tr/swap.ts | 0 .../mobile-kit}/src/i18n/tr/trade.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/accounts.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/activity.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/app.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/approval.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/backup.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/browser.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/buy.ts | 0 .../src/i18n/zh-Hans/collectibles.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/common.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/explore.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/hardware.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/home.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/index.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/kit.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/link.ts | 0 .../src/i18n/zh-Hans/onboarding.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/plugins.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/receive.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/scan.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/security.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/send.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/settings.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/settle.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/social.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/stake.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/swap.ts | 0 .../mobile-kit}/src/i18n/zh-Hans/trade.ts | 0 packages/mobile-kit/src/index.ts | 34 + .../mobile-kit}/src/lib/deeplinks.ts | 0 .../mobile-kit}/src/lib/family-label.ts | 0 .../mobile-kit}/src/lib/notice-routes.ts | 0 .../mobile-kit}/src/lib/security-text.ts | 0 .../mobile-kit}/src/lib/social-errors.ts | 0 .../mobile-kit}/src/lib/swap-links.ts | 0 .../mobile-kit}/src/lib/trade-text.ts | 0 packages/mobile-kit/src/metro.cjs | 82 + .../src/plugins/PluginSandboxes.tsx | 0 .../mobile-kit}/src/plugins/channels.ts | 0 .../mobile-kit}/src/plugins/gunzip.ts | 0 .../mobile-kit}/src/plugins/host.ts | 0 .../mobile-kit}/src/plugins/pako.d.ts | 0 .../mobile-kit}/src/plugins/protocol.ts | 0 .../mobile-kit}/src/plugins/sandbox-entry.ts | 0 .../mobile-kit}/src/polyfills.ts | 0 .../mobile-kit}/src/screens/Accounts.tsx | 0 .../mobile-kit}/src/screens/Activity.tsx | 0 .../mobile-kit}/src/screens/Approval.tsx | 0 .../mobile-kit}/src/screens/Backup.tsx | 0 .../mobile-kit}/src/screens/Browser.tsx | 0 .../mobile-kit}/src/screens/Buy.tsx | 0 .../mobile-kit}/src/screens/Collectibles.tsx | 0 .../mobile-kit}/src/screens/ContactEdit.tsx | 0 .../mobile-kit}/src/screens/Contacts.tsx | 0 .../mobile-kit}/src/screens/DataUse.tsx | 0 .../mobile-kit}/src/screens/Explore.tsx | 0 .../mobile-kit}/src/screens/Hardware.tsx | 0 .../mobile-kit}/src/screens/Home.tsx | 0 .../mobile-kit}/src/screens/LinkedDevices.tsx | 0 .../mobile-kit}/src/screens/Notifications.tsx | 0 .../mobile-kit}/src/screens/Onboarding.tsx | 6 +- .../mobile-kit}/src/screens/Plugins.tsx | 0 .../mobile-kit}/src/screens/Receive.tsx | 0 .../mobile-kit}/src/screens/Scan.tsx | 0 .../mobile-kit}/src/screens/Security.tsx | 0 .../mobile-kit}/src/screens/Send.tsx | 0 .../mobile-kit}/src/screens/Settings.tsx | 6 +- .../mobile-kit}/src/screens/SettleFunding.tsx | 0 .../mobile-kit}/src/screens/Stake.tsx | 0 .../mobile-kit}/src/screens/Swap.tsx | 0 .../mobile-kit}/src/screens/Trade.tsx | 0 .../mobile-kit}/src/shims/no-pbkdf2.js | 0 .../mobile-kit}/src/shims/node-crypto.js | 0 .../mobile-kit}/src/ui/context.tsx | 2 +- .../mobile-kit}/src/ui/icons.tsx | 0 .../mobile-kit}/src/ui/kit.tsx | 0 .../mobile-kit}/src/ui/notifications.ts | 0 .../mobile-kit}/src/ui/theme.ts | 0 .../mobile-kit}/src/ui/ur.tsx | 0 .../mobile-kit}/test/argon2.vitest.ts | 0 .../mobile-kit}/test/bridge.vitest.ts | 2 +- packages/mobile-kit/test/clip.config.ts | 30 + .../mobile-kit}/test/feature-fixtures.ts | 0 .../mobile-kit}/test/features.test.tsx | 0 .../mobile-kit}/test/hardware.test.tsx | 4 +- .../mobile-kit}/test/helpers.tsx | 4 +- .../mobile-kit}/test/i18n-qa.vitest.ts | 0 .../mobile-kit}/test/i18n.vitest.ts | 0 .../mobile-kit/test/jest.setup.cjs | 0 .../mobile-kit}/test/link.test.tsx | 0 .../mobile-kit}/test/link.vitest.ts | 0 .../mobile-kit}/test/links.vitest.ts | 0 .../mobile-kit}/test/notice-routes.vitest.ts | 0 .../mobile-kit}/test/platform.test.tsx | 0 .../mobile-kit}/test/plugin-fixtures.ts | 0 .../mobile-kit}/test/plugins-bridge.vitest.ts | 0 .../test/plugins-sandbox.vitest.ts | 0 .../mobile-kit}/test/screens.test.tsx | 2 +- .../mobile-kit}/test/security-fixtures.ts | 0 .../test/security-plugins.test.tsx | 0 .../mobile-kit}/test/swap-links.vitest.ts | 0 packages/mobile-kit/tsconfig.json | 15 + packages/mobile-kit/vitest.config.ts | 9 + packages/ui/src/context.tsx | 2 +- packages/ui/src/screens/Settings.tsx | 6 +- pnpm-lock.yaml | 1485 +++++++++++++---- .../tools/harness/check.mjs | 10 +- tools/brand/render.mjs | 9 +- tools/harness/check.mjs | 10 +- tools/harness/test/brand.test.mjs | 5 +- tools/harness/test/check.test.mjs | 28 +- 534 files changed, 4470 insertions(+), 1638 deletions(-) create mode 100644 .changeset/desktop-mobile-kits.md delete mode 100644 apps/desktop/scripts/build-native-host.mjs delete mode 100644 apps/desktop/src/renderer/public/icon.svg delete mode 100644 apps/desktop/src/shared/app-config.ts delete mode 100644 apps/desktop/vitest.config.ts delete mode 100644 apps/mobile/src/env.ts delete mode 100644 apps/mobile/vitest.config.ts create mode 100644 packages/config/src/node.ts create mode 100644 packages/desktop-kit/.gitignore create mode 100644 packages/desktop-kit/LICENSE create mode 100644 packages/desktop-kit/NOTICE create mode 100644 packages/desktop-kit/README.md create mode 100644 packages/desktop-kit/package.json rename {apps/desktop => packages/desktop-kit}/scripts/build-inpage.mjs (100%) create mode 100644 packages/desktop-kit/src/builder.ts create mode 100644 packages/desktop-kit/src/csp.ts create mode 100644 packages/desktop-kit/src/electron-vite.ts create mode 100644 packages/desktop-kit/src/index.ts rename {apps/desktop => packages/desktop-kit}/src/main/app-paths.ts (68%) rename {apps/desktop => packages/desktop-kit}/src/main/app-protocol.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/main/biometric.ts (100%) create mode 100644 packages/desktop-kit/src/main/browser/browser-ua.ts rename {apps/desktop => packages/desktop-kit}/src/main/browser/browser.ts (98%) rename {apps/desktop => packages/desktop-kit}/src/main/browser/onemask-relay.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/main/browser/url-policy.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/main/deeplink.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/main/hid.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/main/host/link.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/main/host/wallet.ts (100%) create mode 100644 packages/desktop-kit/src/main/index.ts rename {apps/desktop => packages/desktop-kit}/src/main/ipc-guard.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/main/native-hosts.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/main/storage.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/main/updater.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/native-host/main.ts (100%) create mode 100644 packages/desktop-kit/src/preload/dapp.ts rename {apps/desktop => packages/desktop-kit}/src/preload/inpage-entry.ts (100%) create mode 100644 packages/desktop-kit/src/preload/wallet.ts rename {apps/desktop => packages/desktop-kit}/src/renderer/browser/BrowserChrome.tsx (100%) rename {apps/desktop => packages/desktop-kit}/src/renderer/browser/browser.css (100%) create mode 100644 packages/desktop-kit/src/renderer/browser/mount.tsx create mode 100644 packages/desktop-kit/src/renderer/index.ts rename {apps/desktop => packages/desktop-kit}/src/renderer/shared/bridge.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/renderer/shared/clients.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/renderer/shared/empty-module.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/renderer/shared/globals.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/renderer/shared/hid-agent.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/renderer/shared/mount.tsx (94%) create mode 100644 packages/desktop-kit/src/shared/app-config.ts rename {apps/desktop => packages/desktop-kit}/src/shared/i18n/ar.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/shared/i18n/de.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/shared/i18n/en.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/shared/i18n/es.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/shared/i18n/fr.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/shared/i18n/hi.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/shared/i18n/index.ts (90%) rename {apps/desktop => packages/desktop-kit}/src/shared/i18n/it.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/shared/i18n/ja.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/shared/i18n/ko.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/shared/i18n/pt-BR.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/shared/i18n/tr.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/shared/i18n/zh-Hans.ts (100%) rename {apps/desktop => packages/desktop-kit}/src/shared/ipc.ts (100%) create mode 100644 packages/desktop-kit/test/builder.test.ts create mode 100644 packages/desktop-kit/test/clip.config.ts rename {apps/desktop => packages/desktop-kit}/test/i18n.test.ts (100%) rename {apps/desktop => packages/desktop-kit}/test/ipc.test.ts (100%) rename {apps/desktop => packages/desktop-kit}/test/native-hosts.test.ts (100%) rename {apps/desktop => packages/desktop-kit}/test/onemask-relay.test.ts (100%) rename {apps/desktop => packages/desktop-kit}/test/storage.test.ts (100%) rename {apps/desktop => packages/desktop-kit}/test/url-policy.test.ts (100%) create mode 100644 packages/desktop-kit/tsconfig.json create mode 100644 packages/desktop-kit/vitest.config.ts create mode 100644 packages/mobile-kit/.gitignore create mode 100644 packages/mobile-kit/LICENSE create mode 100644 packages/mobile-kit/NOTICE create mode 100644 packages/mobile-kit/README.md create mode 100644 packages/mobile-kit/babel.config.cjs rename apps/mobile/jest.config.js => packages/mobile-kit/jest.config.cjs (78%) create mode 100644 packages/mobile-kit/package.json rename {apps/mobile => packages/mobile-kit}/scripts/build-inpage.mjs (100%) rename {apps/mobile => packages/mobile-kit}/scripts/build-plugin-sandbox.mjs (100%) rename {apps/mobile => packages/mobile-kit}/src/App.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/background/argon2.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/background/background-task.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/background/device-key.ts (96%) rename {apps/mobile => packages/mobile-kit}/src/background/events.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/background/host.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/background/ledger-ble.ts (72%) rename {apps/mobile => packages/mobile-kit}/src/background/link.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/background/notifications.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/background/passkey.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/background/storage.ts (88%) create mode 100644 packages/mobile-kit/src/brand.ts rename {apps/mobile => packages/mobile-kit}/src/browser/bridge.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/browser/inpage-entry.ts (100%) create mode 100644 packages/mobile-kit/src/env.ts create mode 100644 packages/mobile-kit/src/expo.ts rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/accounts.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/activity.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/app.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/approval.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/backup.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/browser.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/buy.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/collectibles.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/common.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/explore.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/hardware.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/home.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/index.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/kit.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/link.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/onboarding.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/plugins.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/receive.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/scan.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/security.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/send.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/settings.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/settle.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/social.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/stake.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/swap.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ar/trade.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/accounts.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/activity.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/app.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/approval.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/backup.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/browser.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/buy.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/collectibles.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/common.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/explore.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/hardware.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/home.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/index.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/kit.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/link.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/onboarding.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/plugins.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/receive.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/scan.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/security.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/send.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/settings.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/settle.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/social.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/stake.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/swap.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/de/trade.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/device.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/accounts.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/activity.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/app.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/approval.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/backup.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/browser.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/buy.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/collectibles.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/common.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/explore.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/hardware.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/home.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/index.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/kit.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/link.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/onboarding.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/plugins.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/receive.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/scan.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/security.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/send.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/settings.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/settle.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/social.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/stake.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/swap.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/en/trade.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/accounts.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/activity.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/app.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/approval.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/backup.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/browser.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/buy.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/collectibles.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/common.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/explore.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/hardware.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/home.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/index.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/kit.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/link.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/onboarding.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/plugins.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/receive.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/scan.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/security.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/send.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/settings.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/settle.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/social.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/stake.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/swap.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/es/trade.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/accounts.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/activity.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/app.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/approval.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/backup.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/browser.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/buy.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/collectibles.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/common.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/explore.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/hardware.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/home.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/index.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/kit.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/link.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/onboarding.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/plugins.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/receive.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/scan.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/security.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/send.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/settings.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/settle.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/social.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/stake.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/swap.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/fr/trade.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/accounts.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/activity.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/app.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/approval.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/backup.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/browser.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/buy.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/collectibles.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/common.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/explore.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/hardware.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/home.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/index.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/kit.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/link.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/onboarding.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/plugins.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/receive.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/scan.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/security.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/send.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/settings.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/settle.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/social.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/stake.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/swap.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/hi/trade.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/index.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/accounts.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/activity.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/app.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/approval.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/backup.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/browser.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/buy.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/collectibles.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/common.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/explore.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/hardware.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/home.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/index.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/kit.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/link.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/onboarding.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/plugins.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/receive.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/scan.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/security.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/send.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/settings.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/settle.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/social.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/stake.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/swap.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/it/trade.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/accounts.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/activity.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/app.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/approval.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/backup.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/browser.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/buy.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/collectibles.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/common.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/explore.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/hardware.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/home.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/index.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/kit.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/link.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/onboarding.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/plugins.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/receive.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/scan.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/security.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/send.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/settings.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/settle.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/social.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/stake.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/swap.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ja/trade.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/accounts.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/activity.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/app.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/approval.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/backup.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/browser.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/buy.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/collectibles.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/common.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/explore.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/hardware.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/home.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/index.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/kit.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/link.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/onboarding.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/plugins.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/receive.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/scan.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/security.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/send.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/settings.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/settle.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/social.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/stake.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/swap.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/ko/trade.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/accounts.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/activity.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/app.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/approval.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/backup.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/browser.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/buy.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/collectibles.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/common.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/explore.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/hardware.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/home.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/index.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/kit.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/link.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/onboarding.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/plugins.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/receive.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/scan.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/security.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/send.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/settings.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/settle.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/social.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/stake.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/swap.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/pt-BR/trade.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/accounts.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/activity.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/app.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/approval.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/backup.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/browser.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/buy.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/collectibles.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/common.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/explore.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/hardware.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/home.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/index.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/kit.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/link.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/onboarding.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/plugins.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/receive.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/scan.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/security.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/send.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/settings.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/settle.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/social.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/stake.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/swap.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/tr/trade.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/accounts.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/activity.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/app.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/approval.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/backup.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/browser.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/buy.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/collectibles.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/common.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/explore.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/hardware.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/home.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/index.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/kit.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/link.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/onboarding.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/plugins.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/receive.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/scan.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/security.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/send.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/settings.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/settle.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/social.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/stake.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/swap.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/i18n/zh-Hans/trade.ts (100%) create mode 100644 packages/mobile-kit/src/index.ts rename {apps/mobile => packages/mobile-kit}/src/lib/deeplinks.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/lib/family-label.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/lib/notice-routes.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/lib/security-text.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/lib/social-errors.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/lib/swap-links.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/lib/trade-text.ts (100%) create mode 100644 packages/mobile-kit/src/metro.cjs rename {apps/mobile => packages/mobile-kit}/src/plugins/PluginSandboxes.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/plugins/channels.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/plugins/gunzip.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/plugins/host.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/plugins/pako.d.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/plugins/protocol.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/plugins/sandbox-entry.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/polyfills.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Accounts.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Activity.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Approval.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Backup.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Browser.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Buy.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Collectibles.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/ContactEdit.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Contacts.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/DataUse.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Explore.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Hardware.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Home.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/LinkedDevices.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Notifications.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Onboarding.tsx (98%) rename {apps/mobile => packages/mobile-kit}/src/screens/Plugins.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Receive.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Scan.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Security.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Send.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Settings.tsx (98%) rename {apps/mobile => packages/mobile-kit}/src/screens/SettleFunding.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Stake.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Swap.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/screens/Trade.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/shims/no-pbkdf2.js (100%) rename {apps/mobile => packages/mobile-kit}/src/shims/node-crypto.js (100%) rename {apps/mobile => packages/mobile-kit}/src/ui/context.tsx (99%) rename {apps/mobile => packages/mobile-kit}/src/ui/icons.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/ui/kit.tsx (100%) rename {apps/mobile => packages/mobile-kit}/src/ui/notifications.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/ui/theme.ts (100%) rename {apps/mobile => packages/mobile-kit}/src/ui/ur.tsx (100%) rename {apps/mobile => packages/mobile-kit}/test/argon2.vitest.ts (100%) rename {apps/mobile => packages/mobile-kit}/test/bridge.vitest.ts (99%) create mode 100644 packages/mobile-kit/test/clip.config.ts rename {apps/mobile => packages/mobile-kit}/test/feature-fixtures.ts (100%) rename {apps/mobile => packages/mobile-kit}/test/features.test.tsx (100%) rename {apps/mobile => packages/mobile-kit}/test/hardware.test.tsx (97%) rename {apps/mobile => packages/mobile-kit}/test/helpers.tsx (98%) rename {apps/mobile => packages/mobile-kit}/test/i18n-qa.vitest.ts (100%) rename {apps/mobile => packages/mobile-kit}/test/i18n.vitest.ts (100%) rename apps/mobile/test/jest.setup.js => packages/mobile-kit/test/jest.setup.cjs (100%) rename {apps/mobile => packages/mobile-kit}/test/link.test.tsx (100%) rename {apps/mobile => packages/mobile-kit}/test/link.vitest.ts (100%) rename {apps/mobile => packages/mobile-kit}/test/links.vitest.ts (100%) rename {apps/mobile => packages/mobile-kit}/test/notice-routes.vitest.ts (100%) rename {apps/mobile => packages/mobile-kit}/test/platform.test.tsx (100%) rename {apps/mobile => packages/mobile-kit}/test/plugin-fixtures.ts (100%) rename {apps/mobile => packages/mobile-kit}/test/plugins-bridge.vitest.ts (100%) rename {apps/mobile => packages/mobile-kit}/test/plugins-sandbox.vitest.ts (100%) rename {apps/mobile => packages/mobile-kit}/test/screens.test.tsx (99%) rename {apps/mobile => packages/mobile-kit}/test/security-fixtures.ts (100%) rename {apps/mobile => packages/mobile-kit}/test/security-plugins.test.tsx (100%) rename {apps/mobile => packages/mobile-kit}/test/swap-links.vitest.ts (100%) create mode 100644 packages/mobile-kit/tsconfig.json create mode 100644 packages/mobile-kit/vitest.config.ts diff --git a/.changeset/desktop-mobile-kits.md b/.changeset/desktop-mobile-kits.md new file mode 100644 index 0000000..2e9adcb --- /dev/null +++ b/.changeset/desktop-mobile-kits.md @@ -0,0 +1,18 @@ +--- +"@clip-wallet/desktop-kit": patch +"@clip-wallet/mobile-kit": patch +"@clip-wallet/config": patch +"@clip-wallet/i18n": patch +"@clip-wallet/ui": patch +--- + +New packages: `@clip-wallet/desktop-kit` (the Electron app for macOS, Windows and Linux: main process, preloads, pages, +built-in dapp browser, `clipDesktop()` for electron-vite and `electronBuilderConfig()` for electron-builder) and +`@clip-wallet/mobile-kit` (the Expo app for iOS and Android: screens, vault host, in-app browser, `expoConfig()` and +`withClipWallet()` for Metro), both driven by clip.config.ts like `@clip-wallet/extension-kit`. + +`@clip-wallet/config`: `languages`, `appId`, `scheme`, `desktop` / `mobile` id overrides, and the hosted-mode `fees` / +`usage` blocks reserved for Clip Cloud (off by default, never acted on by the kit); `platformIds()`, `enabledLanguages()`; +`@clip-wallet/config/node` with `loadClipConfigSync()` and the shared build-environment helpers. +`@clip-wallet/i18n`: `negotiateLocale` / `resolveLocale` take the languages a wallet offers; `offeredLocales()`. +`@clip-wallet/ui`: Settings → Language lists only the languages clip.config offers. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2b654d..f5d1c0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,10 +101,12 @@ jobs: cache: pnpm - run: pnpm install --frozen-lockfile --filter "@clip-wallet/mobile..." - run: pnpm --filter @clip-wallet/mobile typecheck - - name: Test (vitest + jest-expo) - run: pnpm --filter @clip-wallet/mobile test + - name: Test (vitest + jest-expo; the app's code is @clip-wallet/mobile-kit) + run: pnpm --filter @clip-wallet/mobile-kit test - name: Expo config resolves (icons, splash, plugins) working-directory: apps/mobile + env: + NODE_OPTIONS: --conditions=development run: npx expo config --type prebuild --json > /dev/null services: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 100c755..30fe5d2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -40,7 +40,6 @@ jobs: - packages - apps/extension/src - apps/extension/scripts - - apps/mobile/src - services - tools paths-ignore: diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml index e183228..fbb8fc5 100644 --- a/.github/workflows/desktop.yml +++ b/.github/workflows/desktop.yml @@ -66,7 +66,8 @@ jobs: - name: Typecheck and unit tests run: | pnpm --filter @clip-wallet/desktop typecheck - pnpm --filter @clip-wallet/desktop test + pnpm --filter @clip-wallet/desktop-kit typecheck + pnpm --filter @clip-wallet/desktop-kit test - name: Harness if: runner.os == 'Linux' @@ -97,6 +98,8 @@ jobs: WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }} WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }} CSC_IDENTITY_AUTO_DISCOVERY: "false" + # electron-builder.config.cjs reads clip.config.ts through @clip-wallet/desktop-kit's TypeScript source. + NODE_OPTIONS: --conditions=development run: pnpm exec electron-builder ${{ matrix.dist }} --config electron-builder.config.cjs --publish never - name: Smoke-test the packaged app (starts, serves the wallet page, quits) diff --git a/apps/desktop/.gitignore b/apps/desktop/.gitignore index cd74319..c0a061d 100644 --- a/apps/desktop/.gitignore +++ b/apps/desktop/.gitignore @@ -2,5 +2,3 @@ out/ release/ test-results/ playwright-report/ -# Built from @clip-wallet/1mask at build/typecheck/test time. -src/preload/inpage.generated.ts diff --git a/apps/desktop/electron-builder.config.cjs b/apps/desktop/electron-builder.config.cjs index 4cc555c..3d6b2e8 100644 --- a/apps/desktop/electron-builder.config.cjs +++ b/apps/desktop/electron-builder.config.cjs @@ -1,89 +1,21 @@ /** - * electron-builder (https://www.electron.build/configuration) for Clip Wallet desktop. - * - * macOS dmg + zip, arm64 and x64; hardened runtime with build/entitlements.mac*.plist. - * Windows NSIS installer + zip, x64 and arm64. - * Linux AppImage + deb + tar.gz, x64 and arm64. - * - * Signing and notarization read the standard electron-builder environment variables and are skipped when absent - * (local and CI builds without secrets produce unsigned artifacts): - * macOS signing CSC_LINK + CSC_KEY_PASSWORD (Developer ID Application .p12), or CSC_NAME - * macOS notarization APPLE_API_KEY + APPLE_API_KEY_ID + APPLE_API_ISSUER, or APPLE_ID + APPLE_APP_SPECIFIC_PASSWORD + APPLE_TEAM_ID - * Windows signing WIN_CSC_LINK + WIN_CSC_KEY_PASSWORD (or CSC_LINK on a Windows runner) - * Auto-update metadata is published only for CLIP_UPDATES=1 builds (src/main/updater.ts explains why it is off). - * - * The app ships only out/** (main, preloads and renderer are fully bundled by electron-vite); every npm package is a - * devDependency, so no node_modules go into app.asar and there are no native modules to rebuild. + * electron-builder for Clip Wallet desktop: @clip-wallet/desktop-kit's electronBuilderConfig() from clip.config.ts + * (app id, product name, executable and artifact names, deep-link scheme), with Clip Wallet's own icons rendered from + * brand/ by tools/brand/render.mjs. Signing and notarization use electron-builder's standard environment variables + * and are skipped when absent (see the kit's src/builder.ts). Auto-update metadata only for CLIP_UPDATES=1 builds. */ -const env = process.env; -const macSign = !!(env.CSC_LINK || env.CSC_NAME); -const notarize = macSign && !!((env.APPLE_API_KEY && env.APPLE_API_KEY_ID && env.APPLE_API_ISSUER) || (env.APPLE_ID && env.APPLE_APP_SPECIFIC_PASSWORD && env.APPLE_TEAM_ID)); -const updates = env.CLIP_UPDATES === "1"; +const { electronBuilderConfig } = require("@clip-wallet/desktop-kit/builder"); + +const env = { CLIP_UPDATES_OWNER: "coldai", CLIP_UPDATES_REPO: "clip-wallet", ...process.env }; -/** @type {import("electron-builder").Configuration} */ module.exports = { - appId: "org.coldai.clipwallet.desktop", - productName: "Clip Wallet", - // The workspace package is "@clip-wallet/desktop"; packaged apps get a name that is safe in file paths (deb package, - // Linux executable, NSIS archives). The app's display name stays productName. - extraMetadata: { name: "clip-wallet-desktop" }, - copyright: "Copyright © 2026 ColdAI", - directories: { output: "release", buildResources: "build" }, - files: ["out/**/*", "package.json", "!out/**/*.map"], - asar: true, - // The native-messaging host program runs with ELECTRON_RUN_AS_NODE from a plain file path (src/main/native-hosts.ts). - asarUnpack: ["out/native-host/**"], - npmRebuild: false, - nodeGypRebuild: false, - // Chromium UI locales kept (the app ships 12 languages). macOS names them pt_BR / zh_CN, Windows and Linux pt-BR / zh-CN. - electronLanguages: ["en", "de", "fr", "es", "pt-BR", "pt_BR", "it", "tr", "ja", "ko", "zh-CN", "zh_CN", "ar", "hi"], - protocols: [{ name: "Clip Wallet", schemes: ["clipwallet"] }], - artifactName: "Clip-Wallet-${version}-${os}-${arch}.${ext}", - mac: { - category: "public.app-category.finance", - // Rendered from brand/clip-mark.svg by tools/brand/render.mjs (Apple's 824 px icon grid on a 1024 canvas). - icon: "build/icon-mac.png", - target: [ - { target: "dmg", arch: ["arm64", "x64"] }, - { target: "zip", arch: ["arm64", "x64"] }, - ], - hardenedRuntime: true, - gatekeeperAssess: false, - entitlements: "build/entitlements.mac.plist", - entitlementsInherit: "build/entitlements.mac.inherit.plist", - // null = don't sign (and don't pick up a random identity from the keychain) when no certificate is configured. - ...(macSign ? {} : { identity: null }), - notarize, - extendInfo: { - NSCameraUsageDescription: "Clip Wallet uses the camera only to scan QR codes (WalletConnect codes and Keystone hardware wallets).", - NSMicrophoneUsageDescription: "Clip Wallet doesn't use the microphone. Sites you open in the built-in browser have to ask first.", - }, - }, - dmg: { sign: false }, - win: { - icon: "build/icon.png", - target: [ - { target: "nsis", arch: ["x64", "arm64"] }, - { target: "zip", arch: ["x64", "arm64"] }, - ], - // Editing the .exe resources (icon, version info) and signing use rcedit / signtool: on Windows runners only. - signAndEditExecutable: process.platform === "win32", - }, - nsis: { oneClick: false, perMachine: false, allowToChangeInstallationDirectory: true, deleteAppDataOnUninstall: false }, - linux: { - icon: "build/icon.png", - category: "Finance", - executableName: "clip-wallet", - synopsis: "A calm, non-custodial wallet with a built-in dapp browser. Test networks only.", + ...electronBuilderConfig({ + configFile: "clip.config.ts", + root: __dirname, + env, + icons: { mac: "build/icon-mac.png", win: "build/icon.png", linux: "build/icon.png" }, + copyright: "Copyright © 2026 ColdAI", maintainer: "ColdAI ", - target: [ - { target: "AppImage", arch: ["x64", "arm64"] }, - { target: "deb", arch: ["x64", "arm64"] }, - // Portable archive; also the only Linux format that builds on an Apple-silicon Mac without Rosetta. - { target: "tar.gz", arch: ["x64", "arm64"] }, - ], - }, - // safeStorage uses libsecret (GNOME keyring / KWallet) when present: the deb asks for it. - deb: { depends: ["libgtk-3-0", "libnotify4", "libnss3", "libxss1", "libxtst6", "xdg-utils", "libatspi2.0-0", "libuuid1", "libsecret-1-0"] }, - publish: updates ? [{ provider: "github", owner: env.CLIP_UPDATES_OWNER || "coldai", repo: env.CLIP_UPDATES_REPO || "clip-wallet", releaseType: "release" }] : null, + synopsis: "A calm, non-custodial wallet with a built-in dapp browser. Test networks only.", + }), }; diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts index 1f8c356..b3c4ed3 100644 --- a/apps/desktop/electron.vite.config.ts +++ b/apps/desktop/electron.vite.config.ts @@ -1,16 +1,16 @@ /** - * electron-vite build (https://electron-vite.org/config/): main process (ESM, everything bundled, workspace - * packages are TypeScript sources), sandboxed preloads (CommonJS, one self-contained file each: a sandboxed preload - * can't require chunks), and the renderer pages (wallet, approval, browser toolbar). + * Clip Wallet desktop is @clip-wallet/desktop-kit; this project is the brand. clipDesktop() builds the electron-vite + * config from clip.config.ts: identity, the resolved config for the main process and pages, the native-messaging host + * and the mainnet checklist. * * Build-time switches (public values only; never secrets): * CLIP_WC_PROJECT_ID Reown project id for WalletConnect (unset: WalletConnect says it isn't switched on) - * CLIP_UPDATES=1 turn electron-updater on (only for signed release builds, see src/main/updater.ts) + * CLIP_UPDATES=1 turn electron-updater on (only for signed release builds, see the kit's src/main/updater.ts) * CLIP_EXTENSION_IDS Chromium extension ids allowed to use Clip Desktop over native messaging (Firefox's is fixed) */ import { defineConfig } from "electron-vite"; -import react from "@vitejs/plugin-react"; -import { fileURLToPath } from "node:url"; +import { clipDesktop } from "@clip-wallet/desktop-kit/electron-vite"; +import clipConfig from "./clip.config"; // electron-vite 5's isolated-entries progress line calls process.stdout.clearLine/cursorTo, which exist only on a // TTY (also moveCursor); in CI and piped builds they are missing and the preload build throws. No-ops keep the output plain. @@ -19,67 +19,4 @@ out.clearLine ??= () => true; out.cursorTo ??= () => true; out.moveCursor ??= () => true; -const WC = JSON.stringify(process.env.CLIP_WC_PROJECT_ID?.trim() || ""); -const UPDATES = JSON.stringify(process.env.CLIP_UPDATES === "1"); -// Chromium extension ids the desktop trusts for native messaging (comma-separated; store ids + the dev id). -const EXTENSION_IDS = JSON.stringify(process.env.CLIP_EXTENSION_IDS?.trim() || ""); -/** - * Workspace packages export their TypeScript source under the "development" condition (dist/ is what npm users get); - * this app bundles the source directly, like the extension and the phone (docs: tools/release, packages' exports). - */ -const NODE_CONDITIONS = ["development", "module", "node", "development|production"]; -const BROWSER_CONDITIONS = ["development", "module", "browser", "development|production"]; -const nodeResolve = { resolve: { conditions: NODE_CONDITIONS }, ssr: { resolve: { conditions: NODE_CONDITIONS, externalConditions: ["development", "node"] } } }; - -const empty = fileURLToPath(new URL("./src/renderer/shared/empty-module.ts", import.meta.url)); - -export default defineConfig({ - main: { - ...nodeResolve, - define: { __CLIP_WC_PROJECT_ID__: WC, __CLIP_UPDATES__: UPDATES, __CLIP_EXTENSION_IDS__: EXTENSION_IDS }, - build: { - outDir: "out/main", - externalizeDeps: false, - target: "node22", - sourcemap: false, - rollupOptions: { - input: { index: "src/main/index.ts" }, - external: ["electron", /^node:/], - output: { format: "es", entryFileNames: "[name].mjs", chunkFileNames: "chunks/[name]-[hash].mjs" }, - }, - }, - }, - preload: { - ...nodeResolve, - build: { - outDir: "out/preload", - externalizeDeps: false, - isolatedEntries: true, - sourcemap: false, - rollupOptions: { - input: { wallet: "src/preload/wallet.ts", dapp: "src/preload/dapp.ts" }, - external: ["electron"], - output: { format: "cjs", entryFileNames: "[name].cjs", inlineDynamicImports: true }, - }, - }, - }, - renderer: { - root: "src/renderer", - publicDir: "public", - plugins: [react()], - define: { global: "globalThis", __CLIP_WC_PROJECT_ID__: WC }, - resolve: { conditions: BROWSER_CONDITIONS, alias: { crypto: empty, stream: empty } }, - build: { - outDir: "out/renderer", - target: "chrome140", - sourcemap: false, - rollupOptions: { - input: { - wallet: "src/renderer/wallet/index.html", - approval: "src/renderer/approval/index.html", - browser: "src/renderer/browser/index.html", - }, - }, - }, - }, -}); +export default defineConfig(clipDesktop({ config: clipConfig, root: import.meta.dirname }) as never); diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 2aa771c..38dee18 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -18,53 +18,34 @@ "type": "module", "main": "out/main/index.mjs", "scripts": { - "inpage": "node scripts/build-inpage.mjs", - "build": "node scripts/build-inpage.mjs && electron-vite build && node scripts/build-native-host.mjs", - "dev": "node scripts/build-inpage.mjs && node scripts/build-native-host.mjs && electron-vite dev", + "build": "pnpm --filter @clip-wallet/desktop-kit inpage && NODE_OPTIONS=--conditions=development electron-vite build", + "dev": "pnpm --filter @clip-wallet/desktop-kit inpage && NODE_OPTIONS=--conditions=development electron-vite dev", "start": "electron .", - "typecheck": "node --conditions=development scripts/build-inpage.mjs && tsc -p tsconfig.json --noEmit", - "test": "node scripts/build-inpage.mjs && vitest run", + "typecheck": "pnpm --filter @clip-wallet/desktop-kit inpage && tsc -p tsconfig.json --noEmit", + "test": "echo \"apps/desktop: unit tests live in packages/desktop-kit; pnpm e2e drives this build\"", "e2e": "pnpm build && NODE_OPTIONS=--conditions=development playwright test", - "dist:mac": "pnpm build && electron-builder --mac --config electron-builder.config.cjs", - "dist:win": "pnpm build && electron-builder --win --config electron-builder.config.cjs", - "dist:linux": "pnpm build && electron-builder --linux --config electron-builder.config.cjs", - "dist:dir": "pnpm build && electron-builder --dir --config electron-builder.config.cjs" + "dist:mac": "pnpm build && NODE_OPTIONS=--conditions=development electron-builder --mac --config electron-builder.config.cjs", + "dist:win": "pnpm build && NODE_OPTIONS=--conditions=development electron-builder --win --config electron-builder.config.cjs", + "dist:linux": "pnpm build && NODE_OPTIONS=--conditions=development electron-builder --linux --config electron-builder.config.cjs", + "dist:dir": "pnpm build && NODE_OPTIONS=--conditions=development electron-builder --dir --config electron-builder.config.cjs" }, "devDependencies": { - "@clip-wallet/1mask": "workspace:*", - "@clip-wallet/chains-ton": "workspace:*", "@clip-wallet/config": "workspace:*", - "@clip-wallet/core": "workspace:*", + "@clip-wallet/desktop-kit": "workspace:*", "@clip-wallet/engine": "workspace:*", - "@clip-wallet/features": "workspace:*", - "@clip-wallet/hardware": "workspace:*", - "@clip-wallet/i18n": "workspace:*", "@clip-wallet/link": "workspace:*", - "@clip-wallet/security": "workspace:*", - "@clip-wallet/social": "workspace:*", - "@clip-wallet/ui": "workspace:*", "@clip-wallet/vault": "workspace:*", - "@fontsource-variable/inter": "^5.3.0", - "@ledgerhq/hw-transport": "6.36.0", - "@ledgerhq/hw-transport-webhid": "6.36.0", - "@noble/hashes": "2.4.0", "@playwright/test": "^1.63.0", "@types/node": "^22.20.5", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", - "@vitejs/plugin-react": "^5.1.0", - "buffer": "^6.0.3", "electron": "44.5.1", "electron-builder": "^26.15.3", - "electron-updater": "^6.8.9", "electron-vite": "^5.0.0", - "esbuild": "^0.25.0", "react": "^19.3.0", "react-dom": "^19.3.0", "typescript": "^5.9.3", "viem": "^2.57.2", - "vite": "^7.1.0", - "vitest": "~4.1.11", - "zod": "^4.6.5" + "vite": "^7.1.0" } } diff --git a/apps/desktop/scripts/build-native-host.mjs b/apps/desktop/scripts/build-native-host.mjs deleted file mode 100644 index 7edf895..0000000 --- a/apps/desktop/scripts/build-native-host.mjs +++ /dev/null @@ -1,21 +0,0 @@ -// Bundles src/native-host/main.ts (the native-messaging host program) into out/native-host/clip-native-host.cjs: -// one CommonJS file, Node built-ins only, run by the app's executable with ELECTRON_RUN_AS_NODE=1. -import { build } from "esbuild"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; - -const root = join(dirname(fileURLToPath(import.meta.url)), ".."); -await build({ - entryPoints: [join(root, "src/native-host/main.ts")], - outfile: join(root, "out/native-host/clip-native-host.cjs"), - bundle: true, - // Workspace packages: their "development" export condition is the TypeScript source. - conditions: ["development"], - platform: "node", - format: "cjs", - target: "node22", - minify: false, - legalComments: "none", - logLevel: "warning", -}); -console.log("native host: out/native-host/clip-native-host.cjs"); diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index c52e6c7..b9b9089 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -1,672 +1,2 @@ -/** - * Clip Wallet desktop: main process bootstrap. - * - * wallet window packages/ui WalletApp (same screens, i18n and themes as the extension), clip-app: origin - * approval window packages/ui ApprovalWindowApp, opened by the engine for every request (like the extension's) - * browser window our toolbar + one sandboxed view per tab, one session per site (browser/browser.ts) - * engine + vault here, in the main process (host/wallet.ts); renderers reach it only over checked IPC - * - * Security switches are set before anything loads: single instance, sandbox for every renderer, no remote content - * in app windows (CSP + a request filter on the default session), no new windows, no , no navigation of - * app windows, permission handlers that deny by default. - */ -import { - app, - BrowserWindow, - Menu, - Notification, - dialog, - ipcMain, - nativeTheme, - powerMonitor, - safeStorage, - session, - shell, - systemPreferences, - Tray, - nativeImage, - type IpcMainEvent, - type IpcMainInvokeEvent, - type MenuItemConstructorOptions, - type WebContents, -} from "electron"; -import { mkdtempSync } from "node:fs"; -import { homedir, tmpdir } from "node:os"; -import { join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { randomUUID } from "node:crypto"; -import { createTonModule } from "@clip-wallet/chains-ton"; -import type { LocaleCode } from "@clip-wallet/i18n"; -import type { Notice } from "@clip-wallet/social"; -import { CH, ChromeCall, DesktopCall, HidReply, WalletCall, type DesktopInfo, type Envelope, type HidJob, type OneMaskBoot } from "../shared/ipc"; -import { DESKTOP } from "../shared/app-config"; -import { desktopLocale, desktopT } from "../shared/i18n"; -import { APP_ORIGIN, isAppSender, type Role } from "./ipc-guard"; -import { handleAppScheme, registerAppScheme, walletCsp } from "./app-protocol"; -import { createDesktopWallet, type DesktopWallet } from "./host/wallet"; -import { HidRelay, ledgerDevices, LEDGER_VENDOR_ID } from "./hid"; -import { OneMaskRelay, frameOrigin } from "./browser/onemask-relay"; -import { DappBrowser } from "./browser/browser"; -import { isNavigable } from "./browser/url-policy"; -import { deepLinkFromArgv, parseDeepLink } from "./deeplink"; -import { startAutoUpdate, updatesEnabled } from "./updater"; -import { defaultSocketPath } from "@clip-wallet/link/node"; -import { startDesktopLink } from "./host/link"; -import { allowedOrigins, connectorStatus, installConnector, parseExtensionIds, removeConnector, type ConnectorEnv } from "./native-hosts"; - -declare const __CLIP_EXTENSION_IDS__: string; - -const here = fileURLToPath(new URL(".", import.meta.url)); -const PRELOAD_APP = join(here, "../preload/wallet.cjs"); -const PRELOAD_DAPP = join(here, "../preload/dapp.cjs"); -const RENDERER_ROOT = join(here, "../renderer"); -// The native-messaging host program runs from outside app.asar (electron-builder asarUnpack). -const NATIVE_HOST = join(here, "../native-host/clip-native-host.cjs").replace(/app\.asar([\\/])/, "app.asar.unpacked$1"); -/** Dev / tests only (ignored when packaged): where manifests go, the socket, and extra extension ids to trust. */ -const DEV_NM_HOME = !app.isPackaged ? process.env.CLIP_DESKTOP_NM_HOME : undefined; -const DEV_SOCKET = !app.isPackaged ? process.env.CLIP_DESKTOP_SOCKET : undefined; -const DEV_EXT_IDS = !app.isPackaged ? process.env.CLIP_DESKTOP_EXTENSION_IDS : undefined; -/** - * Tests and smoke runs (honoured in packaged builds too, because it only switches things OFF): no browser-connector - * registration, no link socket, no clipwallet:// registration, so a test never touches this machine's browsers. - */ -const NO_SYSTEM_INTEGRATION = process.env.CLIP_DESKTOP_NO_SYSTEM_INTEGRATION === "1"; -const DEV_URL = !app.isPackaged ? process.env.ELECTRON_RENDERER_URL : undefined; -const DEV_ORIGIN = DEV_URL ? new URL(DEV_URL).origin : undefined; - -/* ------------------------------------------------------------------ before ready */ - -// Tests and side-by-side profiles: a separate data folder. Ignored in packaged builds. -if (!app.isPackaged && process.env.CLIP_DESKTOP_USER_DATA) { - app.setPath("userData", process.env.CLIP_DESKTOP_USER_DATA === "temp" ? mkdtempSync(join(tmpdir(), "clip-desktop-")) : process.env.CLIP_DESKTOP_USER_DATA); -} -app.setAppUserModelId(DESKTOP.appId); -registerAppScheme(); -// Never offer Chromium's own password manager / autofill for anything here. -app.commandLine.appendSwitch("disable-features", "AutofillServerCommunication,PasswordManagerOnboarding"); - -if (!app.requestSingleInstanceLock()) { - app.quit(); -} else { - void main(); -} - -function pageUrl(page: "wallet" | "approval" | "browser", hash = ""): string { - return DEV_URL ? `${DEV_URL}/${page}/index.html${hash}` : `${APP_ORIGIN}/${page}/index.html${hash}`; -} - -async function main() { - const pendingLinks: string[] = []; - const first = deepLinkFromArgv(process.argv); - if (first) pendingLinks.push(first); - app.on("open-url", (e, url) => { - e.preventDefault(); - pendingLinks.push(url); - if (app.isReady()) flushLinks(); - }); - app.on("second-instance", (_e, argv) => { - const link = deepLinkFromArgv(argv); - if (link) { - pendingLinks.push(link); - flushLinks(); - } else showWallet(); - }); - - await app.whenReady(); - // macOS registers clipwallet:// from Info.plist (CFBundleURLTypes); Windows and Linux need it at run time. - if (app.isPackaged && !NO_SYSTEM_INTEGRATION && process.platform !== "darwin" && !app.isDefaultProtocolClient(DESKTOP.scheme)) app.setAsDefaultProtocolClient(DESKTOP.scheme); - handleAppScheme(RENDERER_ROOT, walletCsp(DESKTOP.config.services.mediaProxyUrl)); - - /* ---------------------------------------------------------------- state */ - - const roles = new Map(); - let walletWin: BrowserWindow | null = null; - let approvalWin: BrowserWindow | null = null; - let locale: LocaleCode = desktopLocale(undefined, app.getPreferredSystemLanguages()); - let t = desktopT(locale); - let themePref: "system" | "light" | "dark" = "system"; - const theme = () => (themePref === "system" ? (nativeTheme.shouldUseDarkColors ? "dark" : "light") : themePref); - - const alive = (w: BrowserWindow | null): w is BrowserWindow => !!w && !w.isDestroyed(); - - const hid = new HidRelay({ - target: () => { - const focused = BrowserWindow.getFocusedWindow(); - const pick = [focused, approvalWin, walletWin].find((w) => alive(w) && w.isVisible() && (w === approvalWin || w === walletWin)); - const w = (pick as BrowserWindow | undefined) ?? (alive(walletWin) ? walletWin : null); - return w ? { send: (job: HidJob) => w.webContents.send(CH.hidJob, job) } : null; - }, - }); - - const notifier = { - async show(n: Notice) { - if (!Notification.isSupported()) return; - const note = new Notification({ title: n.title, body: n.body, silent: true }); - note.on("click", () => showWallet(n.route ?? "/")); - note.show(); - }, - }; - - let browser!: DappBrowser; - const wallet: DesktopWallet = createDesktopWallet({ - userData: app.getPath("userData"), - platform: process.platform, - safe: safeStorage, - touchId: { - can: () => process.platform === "darwin" && systemPreferences.canPromptTouchID(), - prompt: (reason) => systemPreferences.promptTouchID(reason), - }, - openApproval: (id) => openApproval(id), - openRoute: (route) => showWallet(route), - broadcast: () => broadcast(), - locked: () => broadcast(), - hidTransport: hid.transport, - notifier, - systemLanguages: () => app.getPreferredSystemLanguages(), - openExternal: async (url) => { - if (new URL(url).protocol === "https:") await shell.openExternal(url); - }, - ...(DESKTOP.wcProjectId ? { wcProjectId: DESKTOP.wcProjectId } : {}), - linkChanged: () => void surfacePairings(), - platformLabel: process.platform === "darwin" ? "macOS" : process.platform === "win32" ? "Windows" : "Linux", - ...(process.env.CLIP_DESKTOP_KNOWN_DAPPS && !app.isPackaged ? { knownDapps: JSON.parse(process.env.CLIP_DESKTOP_KNOWN_DAPPS) as Record } : {}), - }); - - const relay = new OneMaskRelay({ - attach: (port, origin) => wallet.attachDappPort(port, origin), - replyChannel: CH.onemaskToPage, - onActivity: () => browser?.push(), - }); - - /* ---------------------------------------------------------------- linked devices: extension connector */ - - // Extension ids this build trusts: CLIP_EXTENSION_IDS at build time (store ids, plus the unpacked dev id while - // testing); Firefox's add-on id is fixed by the extension's gecko id (wallet@). - const chromiumIds = parseExtensionIds(`${typeof __CLIP_EXTENSION_IDS__ === "string" ? __CLIP_EXTENSION_IDS__ : ""},${DEV_EXT_IDS ?? ""}`); - const firefoxIds = [`wallet@${DESKTOP.config.rdns.split(".").reverse().join(".")}`]; - const socketPath = DEV_SOCKET ?? defaultSocketPath(); - const connector: ConnectorEnv = { - os: process.platform as ConnectorEnv["os"], - rdns: DESKTOP.config.rdns, - home: DEV_NM_HOME ?? homedir(), - ...(process.env.LOCALAPPDATA && !DEV_NM_HOME ? { appData: process.env.LOCALAPPDATA } : DEV_NM_HOME ? { appData: join(DEV_NM_HOME, "AppData", "Local") } : {}), - launcherDir: join(app.getPath("userData"), "native-host"), - execPath: process.execPath, - scriptPath: NATIVE_HOST, - chromiumExtensionIds: chromiumIds, - firefoxAddonIds: firefoxIds, - ...(DEV_SOCKET ? { socketOverride: DEV_SOCKET } : {}), - }; - // Packaged builds (re)register on first run and after every update; dev builds only when asked (Settings). - if (app.isPackaged && !NO_SYSTEM_INTEGRATION && (await wallet.kv.get("clip-desktop/connector-version")) !== app.getVersion()) { - try { - installConnector(connector); - await wallet.kv.set("clip-desktop/connector-version", app.getVersion()); - } catch { - /* Settings → Linked devices shows what's missing and offers "Set up again" */ - } - } - let linkServer: Awaited> | null = null; - try { - if (NO_SYSTEM_INTEGRATION && !DEV_SOCKET) throw new Error("system integration off"); - linkServer = await startDesktopLink(wallet.link, { socketPath, allowedOrigins: allowedOrigins(chromiumIds, firefoxIds) }); - } catch { - linkServer = null; // the socket couldn't be created: the extension says "Clip Desktop isn't running" - } - - /** Pairings already brought to the front (a native pairing from the extension, a scanned code). */ - const surfaced = new Set(); - async function surfacePairings() { - const st = await wallet.link.status().catch(() => null); - for (const p of st?.pairings ?? []) { - if (surfaced.has(p.id) || !["connecting", "compare"].includes(p.state)) continue; - if (p.purpose !== "desktop" && p.state !== "compare") continue; - surfaced.add(p.id); - showWallet(`/settings/devices/pair/${encodeURIComponent(p.id)}`); - } - } - - const channel = `clip-${randomUUID()}`; - const boot: OneMaskBoot = { - channel, - networks: wallet.browserNetworks.map((n) => ({ ...n, rpcUrls: n.rpcUrls.slice(0, 1) })), - identity: DESKTOP.identity, - tonConnect: { key: "clipwallet", appName: "clipwallet", appVersion: app.getVersion(), features: createTonModule().features }, - }; - - browser = new DappBrowser({ - relay, - preload: PRELOAD_DAPP, - chromePreload: PRELOAD_APP, - chromeUrl: pageUrl("browser"), - t: (id, v) => t(id, v), - locale: () => locale, - theme, - kv: wallet.kv, - isKnownScam: (o) => wallet.isKnownScam(o), - scamReasons: async (o) => (await wallet.checkSite(o)).reasons, - connectedOrigins: () => wallet.connectedOrigins(), - featured: () => wallet.featured(), - registerChrome: (wc) => register(wc, "chrome"), - }); - - /* ---------------------------------------------------------------- app windows */ - - function register(wc: WebContents, role: Role) { - roles.set(wc.id, role); - const id = wc.id; - wc.once("destroyed", () => roles.delete(id)); - } - - const appWebPreferences = { - preload: PRELOAD_APP, - sandbox: true, - contextIsolation: true, - nodeIntegration: false, - webviewTag: false, - spellcheck: false, - navigateOnDragDrop: false, - } as const; - - function showWallet(route?: string) { - if (!alive(walletWin)) { - walletWin = new BrowserWindow({ - width: 440, - height: 780, - minWidth: 360, - minHeight: 560, - title: DESKTOP.config.name, - show: false, - backgroundColor: theme() === "dark" ? "#0E0F12" : "#FFFFFF", - webPreferences: appWebPreferences, - }); - register(walletWin.webContents, "wallet"); - void walletWin.loadURL(pageUrl("wallet", route ? `#${route}` : "")); - walletWin.once("ready-to-show", () => walletWin?.show()); - walletWin.on("closed", () => (walletWin = null)); - return; - } - if (route) void walletWin.webContents.executeJavaScript(`location.hash = ${JSON.stringify(route)}`).catch(() => undefined); - if (walletWin.isMinimized()) walletWin.restore(); - walletWin.show(); - walletWin.focus(); - } - - function openApproval(id: string) { - // A request may come from the browser extension (Clip Desktop as its signer) while another app is in front: - // the approval must be where the person is looking. - if (process.platform === "darwin") app.focus({ steal: true }); - if (alive(approvalWin)) { - approvalWin.show(); - approvalWin.focus(); - return; - } - approvalWin = new BrowserWindow({ - width: 400, - height: 680, - minWidth: 360, - minHeight: 520, - title: DESKTOP.config.name, - show: false, - alwaysOnTop: true, - fullscreenable: false, - backgroundColor: theme() === "dark" ? "#0E0F12" : "#FFFFFF", - webPreferences: appWebPreferences, - }); - register(approvalWin.webContents, "approval"); - void approvalWin.loadURL(pageUrl("approval", `#${encodeURIComponent(id)}`)); - approvalWin.once("ready-to-show", () => { - approvalWin?.show(); - approvalWin?.focus(); - }); - approvalWin.on("closed", () => (approvalWin = null)); - } - - async function broadcast() { - for (const w of [walletWin, approvalWin]) if (alive(w)) w.webContents.send(CH.walletChanged); - browser.push(); - if (pendingHandoff && (await wallet.engine.deps.vault.status()) === "unlocked") void openHandoff(pendingHandoff.link, pendingHandoff.url); - // Language and theme follow Settings. - const prefs = await wallet.engine.prefs().catch(() => null); - if (prefs) { - themePref = prefs.theme; - const next = desktopLocale(prefs.locale, app.getPreferredSystemLanguages()); - if (next !== locale) { - locale = next; - t = desktopT(locale); - buildMenu(); - buildTray(); - } - } - } - - /* ---------------------------------------------------------------- sessions */ - - const ses = session.defaultSession; - // App windows load nothing from the network: only the bundle (clip-app:), data:/blob:, devtools, and NFT images - // through the configured media proxy. - const mediaOrigin = DESKTOP.config.services.mediaProxyUrl ? new URL(DESKTOP.config.services.mediaProxyUrl).origin : null; - ses.webRequest.onBeforeRequest({ urls: ["http://*/*", "https://*/*", "ws://*/*", "wss://*/*", "file://*/*", "ftp://*/*"] }, (d, cb) => { - const origin = (() => { - try { - return new URL(d.url).origin; - } catch { - return ""; - } - })(); - const ok = (DEV_ORIGIN && origin === DEV_ORIGIN) || (mediaOrigin && origin === mediaOrigin && (d.resourceType === "image" || d.resourceType === "media")); - cb({ cancel: !ok }); - }); - ses.setPermissionRequestHandler((wc, permission, callback, details) => { - const isApp = roles.has(wc.id) && details.isMainFrame && (details.requestingUrl.startsWith(`${APP_ORIGIN}/`) || (!!DEV_ORIGIN && details.requestingUrl.startsWith(DEV_ORIGIN))); - const media = (details as { mediaTypes?: string[] }).mediaTypes ?? []; - // The wallet's own QR scanner (WalletConnect, Keystone): camera only, never the microphone. - if (isApp && permission === "media" && media.length > 0 && media.every((m) => m === "video")) return callback(true); - if (isApp && permission === "clipboard-sanitized-write") return callback(true); - callback(false); - }); - ses.setPermissionCheckHandler((wc, permission, requestingOrigin) => { - if (!wc || !roles.has(wc.id)) return false; - const isApp = requestingOrigin === APP_ORIGIN || requestingOrigin === `${APP_ORIGIN}/` || (!!DEV_ORIGIN && requestingOrigin.startsWith(DEV_ORIGIN)); - return isApp && (permission === "hid" || permission === "media" || permission === "clipboard-sanitized-write"); - }); - // WebHID: Ledger only, for our own pages; the picker is a native dialog with an explicit choice. - const pickedHid = new Set(); - ses.setDevicePermissionHandler((d) => { - const isApp = d.origin === APP_ORIGIN || d.origin === `${APP_ORIGIN}/` || (!!DEV_ORIGIN && d.origin.startsWith(DEV_ORIGIN)); - const dev = d.device as { vendorId?: number; deviceId?: string }; - // Only a Ledger the user picked in this run's chooser (so the approval window can reopen it without asking). - return isApp && d.deviceType === "hid" && dev.vendorId === LEDGER_VENDOR_ID && !!dev.deviceId && pickedHid.has(dev.deviceId); - }); - ses.on("select-hid-device", (event, details, callback) => { - event.preventDefault(); - // Only our windows use the default session (dapp tabs have their own sessions, which deny HID outright). - const devices = ledgerDevices(details.deviceList); - const parent = BrowserWindow.getFocusedWindow() ?? walletWin ?? undefined; - if (!devices.length) { - const opts = { type: "info" as const, message: t("d.hid.title"), detail: t("d.hid.none"), buttons: ["OK"] }; - void (parent ? dialog.showMessageBox(parent, opts) : dialog.showMessageBox(opts)); - return callback(""); - } - const opts = { - type: "question" as const, - message: t("d.hid.title"), - detail: t("d.hid.body", { name: DESKTOP.config.name }), - buttons: [...devices.map((d) => d.name || "Ledger"), t("d.common.cancel")], - cancelId: devices.length, - defaultId: devices.length, - }; - void (parent ? dialog.showMessageBox(parent, opts) : dialog.showMessageBox(opts)).then((r) => { - const d = devices[r.response]; - if (!d) return callback(""); - pickedHid.add(d.deviceId); - callback(d.deviceId); - }); - }); - ses.on("select-serial-port", (e, _l, _wc, cb) => (e.preventDefault(), cb(""))); - ses.on("select-usb-device", (e, _d, cb) => (e.preventDefault(), cb())); - ses.on("will-download", (e) => e.preventDefault()); - - app.on("web-contents-created", (_e, wc) => { - wc.on("will-attach-webview", (e) => e.preventDefault()); - // App windows never navigate and never open windows. (Dapp tabs replace both handlers in browser.ts.) - wc.setWindowOpenHandler(({ url }) => { - if (roles.has(wc.id) && /^https:\/\//.test(url)) void shell.openExternal(url); - return { action: "deny" }; - }); - wc.on("will-navigate", (e) => { - if (!browser.isTabContents(wc.id)) e.preventDefault(); - }); - }); - - /* ---------------------------------------------------------------- IPC */ - - const toEnvelope = (e: unknown): Envelope => { - const u = e as { userMessage?: unknown; code?: unknown; msg?: unknown } | null; - if (u && typeof u.userMessage === "string" && typeof u.code === "string") - return { ok: false, error: { userMessage: u.userMessage, code: u.code, ...(u.msg !== undefined ? { msg: u.msg } : {}) } }; - return { ok: false, error: { userMessage: "Something went wrong. Please try again.", code: "internal" } }; - }; - const denied: Envelope = { ok: false, error: { userMessage: "Something went wrong. Please try again.", code: "bus/forbidden" } }; - const invalid: Envelope = { ok: false, error: { userMessage: "Something went wrong. Please try again.", code: "bus/invalid" } }; - const guard = (e: IpcMainEvent | IpcMainInvokeEvent, allowed: Role[]) => isAppSender(e as never, roles, allowed, DEV_ORIGIN); - - ipcMain.handle(CH.walletCall, async (e, raw: unknown): Promise => { - if (!guard(e, ["wallet", "approval"])) return denied; - const msg = WalletCall.safeParse(raw); - if (!msg.success) return invalid; - try { - return { ok: true, data: await wallet.handle(msg.data as never) }; - } catch (err) { - return toEnvelope(err); - } - }); - - ipcMain.handle(CH.desktopCall, async (e, raw: unknown): Promise => { - if (!guard(e, ["wallet", "approval"])) return denied; - const m = DesktopCall.safeParse(raw); - if (!m.success) return invalid; - try { - const c = m.data; - switch (c.op) { - case "info": { - const info: DesktopInfo = { - platform: process.platform, - appVersion: app.getVersion(), - electron: process.versions.electron, - chrome: process.versions.chrome, - storage: wallet.protection, - biometrics: wallet.biometrics, - updates: updatesEnabled() ? "enabled" : "disabled", - }; - return { ok: true, data: info }; - } - case "openBrowser": - if (c.url && !isNavigable(c.url)) return invalid; - browser.newTab(c.url); - return { ok: true }; - case "openExternal": - if (!c.url.startsWith("https://")) return invalid; - await shell.openExternal(c.url); - return { ok: true }; - case "showWallet": - showWallet(c.route); - return { ok: true }; - case "bioEnroll": - return { ok: true, data: await wallet.bioEnroll(c.ceremonyId, c.prfInput, t("d.bio.enroll", { name: DESKTOP.config.name })) }; - case "bioEvaluate": - await wallet.bioEvaluate(c.ceremonyId, c.credentialId, c.prfInput, t("d.bio.unlock", { name: DESKTOP.config.name })); - return { ok: true }; - case "connectorStatus": - return { ok: true, data: connectorStatus(connector) }; - case "connectorRepair": { - if (NO_SYSTEM_INTEGRATION && !DEV_NM_HOME) return invalid; - const v = installConnector(connector); - await wallet.kv.set("clip-desktop/connector-version", app.getVersion()); - return { ok: true, data: v }; - } - case "connectorRemove": - return { ok: true, data: removeConnector(connector) }; - case "closeSelf": - BrowserWindow.fromWebContents(e.sender)?.close(); - return { ok: true }; - } - } catch (err) { - return toEnvelope(err); - } - }); - - ipcMain.on(CH.hidReply, (e, raw: unknown) => { - if (!guard(e, ["wallet", "approval"])) return; - const r = HidReply.safeParse(raw); - if (r.success) hid.onReply(r.data); - }); - - ipcMain.handle(CH.chromeCall, async (e, raw: unknown): Promise => { - if (!guard(e, ["chrome"])) return denied; - const m = ChromeCall.safeParse(raw); - if (!m.success) return invalid; - const c = m.data; - switch (c.op) { - case "state": - return { ok: true, data: await browser.state() }; - case "newTab": - if (c.url !== undefined && !isNavigable(c.url)) return invalid; - browser.newTab(c.url); - return { ok: true }; - case "closeTab": - browser.closeTab(c.tabId); - return { ok: true }; - case "selectTab": - browser.selectTab(c.tabId); - return { ok: true }; - case "navigate": - return browser.navigate(c.tabId, c.input) ? { ok: true } : { ok: false, error: { userMessage: t("d.browser.badUrl"), code: "browser/bad-url" } }; - case "back": - browser.back(c.tabId); - return { ok: true }; - case "forward": - browser.forward(c.tabId); - return { ok: true }; - case "reload": - browser.reload(c.tabId); - return { ok: true }; - case "stop": - browser.stop(c.tabId); - return { ok: true }; - case "toggleBookmark": - await browser.toggleBookmark(c.tabId); - return { ok: true }; - case "removeBookmark": - await browser.removeBookmark(c.url); - return { ok: true }; - case "answer": - browser.answer(c.promptId, c.choice); - return { ok: true }; - case "openWallet": - showWallet(c.route); - return { ok: true }; - } - }); - - // 1Mask: boot config and requests, only from the top frame of a dapp tab. - ipcMain.on(CH.onemaskBoot, (e) => { - e.returnValue = browser.isTabContents(e.sender.id) && frameOrigin(e.sender as never, e.senderFrame as never) ? boot : null; - }); - ipcMain.on(CH.onemaskToMain, (e, raw: unknown) => { - if (!browser.isTabContents(e.sender.id)) return; - relay.onMessage(e.sender as never, e.senderFrame as never, raw); - }); - - /* ---------------------------------------------------------------- lifecycle */ - - powerMonitor.on("lock-screen", () => void wallet.lock()); - powerMonitor.on("suspend", () => void wallet.lock()); - nativeTheme.on("updated", () => browser.push()); - - function buildMenu() { - const name = DESKTOP.config.name; - const template: MenuItemConstructorOptions[] = [ - ...(process.platform === "darwin" - ? [{ label: name, submenu: [{ role: "about" }, { type: "separator" }, { role: "hide" }, { role: "hideOthers" }, { type: "separator" }, { label: t("d.menu.quit", { name }), role: "quit" }] } as MenuItemConstructorOptions] - : []), - { - label: t("d.menu.wallet"), - submenu: [ - { label: t("d.menu.wallet"), accelerator: "CmdOrCtrl+1", click: () => showWallet() }, - { label: t("d.menu.lock"), accelerator: "CmdOrCtrl+Shift+L", click: () => void wallet.lock() }, - ...(process.platform === "darwin" ? [] : [{ type: "separator" } as MenuItemConstructorOptions, { label: t("d.menu.quit", { name }), role: "quit" } as MenuItemConstructorOptions]), - ], - }, - { - label: t("d.menu.browser"), - submenu: [ - { label: t("d.menu.browser"), accelerator: "CmdOrCtrl+2", click: () => (browser.window ? browser.focus() : browser.newTab()) }, - { label: t("d.menu.newTab"), accelerator: "CmdOrCtrl+T", click: () => browser.newTab() }, - ], - }, - { label: t("d.menu.edit"), role: "editMenu" }, - { label: t("d.menu.view"), submenu: [{ role: "resetZoom" }, { role: "zoomIn" }, { role: "zoomOut" }, { type: "separator" }, { role: "togglefullscreen" }, ...(app.isPackaged ? [] : [{ role: "toggleDevTools" } as MenuItemConstructorOptions])] }, - { label: t("d.menu.window"), role: "windowMenu" }, - ]; - Menu.setApplicationMenu(Menu.buildFromTemplate(template)); - } - - /** - * "Continue on this device": the token can only be checked with the unlocked wallet, so a locked wallet asks to - * unlock first and the handoff waits. Linked devices then shows "Continue on "; one tap restores the - * connection for that site and opens it in the built-in browser. - */ - let pendingHandoff: { link: string; url: string } | null = null; - async function openHandoff(raw: string, url: string) { - if ((await wallet.engine.deps.vault.status()) !== "unlocked") { - pendingHandoff = { link: raw, url }; - showWallet(); - return; - } - pendingHandoff = null; - try { - await wallet.link.handoffOpen(raw); - showWallet("/settings/devices"); - } catch { - browser.newTab(url); - } - } - - /* tray / menu bar: open the wallet or the browser, lock, quit */ - let tray: Tray | null = null; - function buildTray() { - const name = DESKTOP.config.name; - if (!tray) { - const icon = nativeImage.createFromPath(join(RENDERER_ROOT, "tray", process.platform === "darwin" ? "trayTemplate.png" : "tray.png")); - if (process.platform === "darwin") icon.setTemplateImage(true); - tray = new Tray(icon); - tray.setToolTip(name); - tray.on("click", () => showWallet()); - } - tray.setContextMenu( - Menu.buildFromTemplate([ - { label: t("d.tray.open", { name }), click: () => showWallet() }, - { label: t("d.menu.browser"), click: () => (browser.window ? browser.focus() : browser.newTab()) }, - { type: "separator" }, - { label: t("d.menu.lock"), click: () => void wallet.lock() }, - { label: t("d.menu.quit", { name }), role: "quit" }, - ]), - ); - } - - function flushLinks() { - while (pendingLinks.length) { - const link = parseDeepLink(pendingLinks.shift()); - if (!link) continue; - if (link.kind === "browse") browser.newTab(link.url); - else if (link.kind === "trade") showWallet(link.route); - else if (link.kind === "handoff") void openHandoff(link.link, link.url); - else if (link.kind === "pair") - void wallet.link.scan(link.uri).then( - (p) => (surfaced.add(p.id), showWallet(`/settings/devices/pair/${encodeURIComponent(p.id)}`)), - () => showWallet("/settings/devices"), - ); - else { - showWallet(); - void wallet.pairWalletConnect(link.uri).catch(() => showWallet("/scan")); - } - } - } - - app.on("activate", () => showWallet()); - app.on("window-all-closed", () => { - if (process.platform !== "darwin") app.quit(); - }); - app.on("before-quit", () => { - wallet.dispose(); - void linkServer?.stop(); - }); - - await broadcast().catch(() => undefined); - buildMenu(); - buildTray(); - showWallet(); - flushLinks(); - void startAutoUpdate(); -} +// Clip Wallet desktop is @clip-wallet/desktop-kit; this project is the brand (clip.config.ts, icons). +import "@clip-wallet/desktop-kit/main"; diff --git a/apps/desktop/src/preload/dapp.ts b/apps/desktop/src/preload/dapp.ts index 6b17e50..5cbca18 100644 --- a/apps/desktop/src/preload/dapp.ts +++ b/apps/desktop/src/preload/dapp.ts @@ -1,45 +1 @@ -/** - * Preload of every dapp tab (sandboxed, isolated world; the page cannot see this script or anything it holds). - * - * 1. Asks the main process for the 1Mask boot config. The main process answers only for the top frame of a - * dapp tab on an http(s) origin; preloads don't run in iframes at all (no nodeIntegrationInSubFrames). - * 2. Runs the 1Mask inpage providers in the page's MAIN world, synchronously, before any page script. - * 3. Runs 1Mask's content bridge here, in the ISOLATED world: it accepts only same-window postMessages on the - * run's channel that pass the strict schema, and relays them to the main process over IPC. The main process - * replaces the origin with the frame's committed origin (onemask-relay.ts). - * - * Nothing is exposed to the page: no contextBridge.exposeInMainWorld, so window.require, ipcRenderer, process and - * electron stay out of its reach. - */ -import { contextBridge, ipcRenderer } from "electron"; -import { createContentBridge, type RuntimePort } from "@clip-wallet/1mask/content"; -import { CH, type OneMaskBoot } from "../shared/ipc"; -import { inpageMain } from "./inpage.generated"; - -function start() { - if (location.protocol !== "https:" && location.protocol !== "http:") return; - let boot: OneMaskBoot | null = null; - try { - boot = ipcRenderer.sendSync(CH.onemaskBoot) as OneMaskBoot | null; - } catch { - boot = null; - } - if (!boot || typeof boot.channel !== "string") return; - - contextBridge.executeInMainWorld({ func: inpageMain, args: [boot] }); - - const messageListeners = new Set<(m: unknown) => void>(); - const disconnectListeners = new Set<() => void>(); - ipcRenderer.on(CH.onemaskToPage, (_e, msg: unknown) => { - for (const l of messageListeners) l(msg); - }); - const port: RuntimePort = { - postMessage: (message) => ipcRenderer.send(CH.onemaskToMain, message), - onMessage: { addListener: (cb) => void messageListeners.add(cb) }, - onDisconnect: { addListener: (cb) => void disconnectListeners.add(cb) }, - disconnect: () => undefined, - }; - createContentBridge({ channel: boot.channel, connect: () => port }); -} - -start(); +import "@clip-wallet/desktop-kit/preload/dapp"; diff --git a/apps/desktop/src/preload/wallet.ts b/apps/desktop/src/preload/wallet.ts index a927899..4bde001 100644 --- a/apps/desktop/src/preload/wallet.ts +++ b/apps/desktop/src/preload/wallet.ts @@ -1,40 +1 @@ -/** - * Preload of the wallet, approval and browser-toolbar windows (sandboxed, isolated world). It exposes one small, - * typed bridge object; the renderer never gets ipcRenderer itself. Every call is checked again in the main - * process (schema, then sender: ipc-guard.ts). - */ -import { contextBridge, ipcRenderer } from "electron"; -import { CH, type ChromeCall, type ChromeState, type DesktopCall, type HidJob, type HidReply } from "../shared/ipc"; - -const api = { - platform: process.platform, - /** Wallet / features / social / security / hardware message → Envelope. */ - call: (msg: unknown): Promise => ipcRenderer.invoke(CH.walletCall, msg), - /** Desktop-only calls → Envelope. */ - desktop: (msg: DesktopCall): Promise => ipcRenderer.invoke(CH.desktopCall, msg), - onChange(cb: () => void): () => void { - const h = () => cb(); - ipcRenderer.on(CH.walletChanged, h); - return () => void ipcRenderer.removeListener(CH.walletChanged, h); - }, - /** Ledger APDU jobs (the WebHID half of hid.ts). */ - onHidJob(handler: (job: HidJob) => Promise): void { - ipcRenderer.on(CH.hidJob, (_e, job: HidJob) => { - void handler(job).then( - (r) => ipcRenderer.send(CH.hidReply, r), - (e: unknown) => ipcRenderer.send(CH.hidReply, { id: job.id, ok: false, name: "Error", message: String((e as Error)?.message ?? e).slice(0, 500) }), - ); - }); - }, - /** Browser toolbar only. */ - chrome: (msg: ChromeCall): Promise => ipcRenderer.invoke(CH.chromeCall, msg), - onChromeState(cb: (s: ChromeState) => void): () => void { - const h = (_e: unknown, s: ChromeState) => cb(s); - ipcRenderer.on(CH.chromeState, h); - return () => void ipcRenderer.removeListener(CH.chromeState, h); - }, -}; - -export type ClipDesktopApi = typeof api; - -contextBridge.exposeInMainWorld("clipDesktop", api); +import "@clip-wallet/desktop-kit/preload/wallet"; diff --git a/apps/desktop/src/renderer/approval/index.html b/apps/desktop/src/renderer/approval/index.html index 0d19f57..0f72ef3 100644 --- a/apps/desktop/src/renderer/approval/index.html +++ b/apps/desktop/src/renderer/approval/index.html @@ -3,7 +3,7 @@ - + Request diff --git a/apps/desktop/src/renderer/approval/main.tsx b/apps/desktop/src/renderer/approval/main.tsx index 5ca6be1..9eb3f6b 100644 --- a/apps/desktop/src/renderer/approval/main.tsx +++ b/apps/desktop/src/renderer/approval/main.tsx @@ -1,3 +1,3 @@ -import { mountApproval } from "../shared/mount"; +import { mountApproval } from "@clip-wallet/desktop-kit/renderer"; void mountApproval(); diff --git a/apps/desktop/src/renderer/browser/main.tsx b/apps/desktop/src/renderer/browser/main.tsx index 21dbdea..b2aa8e2 100644 --- a/apps/desktop/src/renderer/browser/main.tsx +++ b/apps/desktop/src/renderer/browser/main.tsx @@ -1,12 +1,3 @@ -import "../shared/globals"; -import "@fontsource-variable/inter"; -import "./browser.css"; -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import { BrowserChrome } from "./BrowserChrome"; +import { mountBrowserChrome } from "@clip-wallet/desktop-kit/renderer"; -createRoot(document.getElementById("root")!).render( - - - , -); +mountBrowserChrome(); diff --git a/apps/desktop/src/renderer/public/icon.svg b/apps/desktop/src/renderer/public/icon.svg deleted file mode 100644 index c6c348f..0000000 --- a/apps/desktop/src/renderer/public/icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - Clip Wallet - - - diff --git a/apps/desktop/src/renderer/wallet/index.html b/apps/desktop/src/renderer/wallet/index.html index 3a0d3e5..4c64780 100644 --- a/apps/desktop/src/renderer/wallet/index.html +++ b/apps/desktop/src/renderer/wallet/index.html @@ -3,8 +3,8 @@ - - Clip Wallet + + %CLIP_WALLET_NAME%
diff --git a/apps/desktop/src/renderer/wallet/main.tsx b/apps/desktop/src/renderer/wallet/main.tsx index 5c581dc..fd4ecc3 100644 --- a/apps/desktop/src/renderer/wallet/main.tsx +++ b/apps/desktop/src/renderer/wallet/main.tsx @@ -1,3 +1,3 @@ -import { mountWallet } from "../shared/mount"; +import { mountWallet } from "@clip-wallet/desktop-kit/renderer"; void mountWallet(); diff --git a/apps/desktop/src/shared/app-config.ts b/apps/desktop/src/shared/app-config.ts deleted file mode 100644 index 83f4eb7..0000000 --- a/apps/desktop/src/shared/app-config.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Build-time settings shared by the main process and the renderers. Public identifiers only, never secrets. - * `__CLIP_WC_PROJECT_ID__` is replaced at build time from CLIP_WC_PROJECT_ID (electron.vite.config.ts); unset - * means WalletConnect is switched off and the UI says so. - */ -import clipConfig from "../../clip.config"; - -declare const __CLIP_WC_PROJECT_ID__: string | undefined; -const wcProjectId = (typeof __CLIP_WC_PROJECT_ID__ !== "undefined" && __CLIP_WC_PROJECT_ID__) || undefined; - -export const DESKTOP = { - config: { ...clipConfig, walletConnect: { ...clipConfig.walletConnect, projectId: wcProjectId } }, - wcProjectId, - /** Placeholder metadata URL shown to WalletConnect peers until the app has a site. */ - siteUrl: "https://clipwallet.example", - iconUrl: "https://clipwallet.example/icon.png", - /** EIP-6963 / Wallet Standard identity (icon must be a data URI). */ - identity: { - name: clipConfig.name, - rdns: clipConfig.rdns, - icon: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMTI4IDEyOCI+CiAgPHRpdGxlPkNsaXAgV2FsbGV0PC90aXRsZT4KICA8cmVjdCB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgcng9IjMwIiBmaWxsPSIjRkYzQzAwIi8+CiAgPGcgZmlsbD0iI0ZGRkZGRiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDY0IDY0KSBza2V3WCgtNikgdHJhbnNsYXRlKC02NCAtNjQpIj48cGF0aCBkPSJNNzIgMThDNDYgMzAgMzIgNTAgMzIgNzJjMCA3IDEgMTIgMyAxNmgzN3oiLz48cGF0aCBkPSJNODQgNDBjMTAgMTIgMTQgMjYgMTMgNDAtNCA0LTkgNy0xMyA4eiIvPjxwYXRoIGQ9Ik0yNCA5OWg4MmMtNiA3LTE1IDExLTI2IDExSDQ4Yy0xMSAwLTE5LTQtMjQtMTF6Ii8+PC9nPgo8L3N2Zz4K", - }, - /** Deep links: clipwallet://wc?uri=…, clipwallet://browse?url=…, clipwallet://trade#offer=… */ - scheme: "clipwallet", - /** Secure Trade share links open the app. */ - tradeLinkBase: "clipwallet://trade", - /** Bundle / app id (electron-builder appId, Windows AppUserModelID). */ - appId: "org.coldai.clipwallet.desktop", -} as const; diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index 9970de4..f400f2a 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -6,6 +6,6 @@ "types": ["node"], "noEmit": true }, - "include": ["src", "test", "e2e", "clip.config.ts", "electron.vite.config.ts", "vitest.config.ts", "playwright.config.ts"], + "include": ["src", "e2e", "clip.config.ts", "electron.vite.config.ts", "playwright.config.ts"], "exclude": ["out", "release", "node_modules"] } diff --git a/apps/desktop/vitest.config.ts b/apps/desktop/vitest.config.ts deleted file mode 100644 index fcd6cd1..0000000 --- a/apps/desktop/vitest.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { include: ["test/**/*.test.ts"], environment: "node" }, -}); diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore index d4bd8d2..ee71864 100644 --- a/apps/mobile/.gitignore +++ b/apps/mobile/.gitignore @@ -3,7 +3,3 @@ ios/ android/ .expo/ dist/ -# Built from @clip-wallet/1mask at typecheck/test/start time. -src/browser/inpage.generated.ts -# Built from @clip-wallet/plugins (SES + runtime) with the inpage bundle. -src/plugins/sandbox.generated.ts diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 0a5eb0d..e1e771a 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -1,87 +1,18 @@ -import type { ExpoConfig } from "expo/config"; +import { expoConfig } from "@clip-wallet/mobile-kit/expo"; /** - * Clip Wallet mobile (iOS + Android). Testnets only. Native projects are generated (`expo prebuild`), not - * committed. Optional build-time env (never committed, see .env.example): - * EXPO_PUBLIC_WC_PROJECT_ID Reown project id; WalletConnect is switched off (said plainly) without it. - * CLIP_ASSOCIATED_DOMAIN e.g. clipwallet.example — adds applinks:/webcredentials: (universal links, - * passkeys). Needs a signed build and the domain's AASA / assetlinks files. + * Clip Wallet mobile (iOS + Android). Testnets only. @clip-wallet/mobile-kit builds the Expo config from clip.config.ts + * (name, ids, scheme, icons, splash, permissions); native projects are generated (`expo prebuild`), not committed. + * Optional build-time env (never committed, see .env.example): EXPO_PUBLIC_WC_PROJECT_ID, EXPO_PUBLIC_PASSKEY_RP_ID, + * CLIP_ASSOCIATED_DOMAIN. */ -const domain = process.env.CLIP_ASSOCIATED_DOMAIN; - -const CAMERA = "Scan a connection code from an app, a trade link, or your Keystone's QR codes."; -const BLUETOOTH = "Connect to your Ledger over Bluetooth to add its accounts and approve with it."; - -const config: ExpoConfig = { - name: "Clip Wallet", - slug: "clip-wallet", - scheme: "clipwallet", - version: "0.1.0", - orientation: "portrait", - // Brand assets are rendered from brand/*.svg by tools/brand/render.mjs (never edit the PNGs by hand). - icon: "./assets/icon.png", - userInterfaceStyle: "automatic", - ios: { - bundleIdentifier: "org.coldai.clipwallet", - // iOS 18 appearance variants: https://docs.expo.dev/develop/user-interface/splash-screen-and-app-icon/ - icon: { light: "./assets/icon.png", dark: "./assets/icon-dark.png", tinted: "./assets/icon-tinted.png" }, - supportsTablet: false, - ...(domain ? { associatedDomains: [`applinks:${domain}`, `webcredentials:${domain}`] } : {}), - infoPlist: { - NSFaceIDUsageDescription: "Unlock Clip Wallet with Face ID instead of typing your password.", - NSCameraUsageDescription: CAMERA, - // Ledger over Bluetooth (react-native-ble-plx). Its config plugin writes the same key; set here so the text is ours. - NSBluetoothAlwaysUsageDescription: BLUETOOTH, - // The in-app browser loads https dapps; plain http only for a dapp served from this Mac / LAN while developing. - NSAppTransportSecurity: { NSAllowsArbitraryLoads: false, NSAllowsLocalNetworking: true }, - ITSAppUsesNonExemptEncryption: false, +export default () => + expoConfig({ + configFile: "clip.config.ts", + root: __dirname, + overrides: { + // Brand assets are rendered from brand/*.svg by tools/brand/render.mjs (never edit the PNGs by hand). + // iOS 18 appearance variants: https://docs.expo.dev/develop/user-interface/splash-screen-and-app-icon/ + ios: { icon: { light: "./assets/icon.png", dark: "./assets/icon-dark.png", tinted: "./assets/icon-tinted.png" } }, }, - }, - android: { - package: "org.coldai.clipwallet", - // Glyph inside the 66% safe zone on ColdAI orange; monochrome for Android 13+ themed icons. - adaptiveIcon: { - foregroundImage: "./assets/adaptive-icon.png", - monochromeImage: "./assets/adaptive-monochrome.png", - backgroundColor: "#FF3C00", - }, - allowBackup: false, - // Bluetooth for Ledger: BLUETOOTH_SCAN (added with neverForLocation by the ble-plx plugin below; listing it here - // would stop the plugin adding that flag) and - // BLUETOOTH_CONNECT on Android 12+, asked at run time; Android 11 and lower need fine location for BLE scans. - permissions: ["android.permission.CAMERA", "android.permission.USE_BIOMETRIC", "android.permission.BLUETOOTH_CONNECT"], - intentFilters: domain - ? [ - { - action: "VIEW", - autoVerify: true, - data: [ - { scheme: "https", host: domain, pathPrefix: "/wc" }, - { scheme: "https", host: domain, pathPrefix: "/trade" }, - ], - category: ["BROWSABLE", "DEFAULT"], - }, - ] - : [], - }, - plugins: [ - // Splash: white sail on ColdAI orange, same in dark mode. - ["expo-splash-screen", { image: "./assets/splash-icon.png", imageWidth: 160, backgroundColor: "#FF3C00", resizeMode: "contain" }], - ["expo-secure-store", { configureAndroidBackup: true, faceIDPermission: "Unlock Clip Wallet with Face ID instead of typing your password." }], - ["expo-local-authentication", { faceIDPermission: "Unlock Clip Wallet with Face ID instead of typing your password." }], - ["expo-camera", { cameraPermission: CAMERA, microphonePermission: false, recordAudioAndroid: false }], - // Ledger over Bluetooth. Foreground only (no background modes); neverForLocation: we never derive location. - ["react-native-ble-plx", { isBackgroundEnabled: false, modes: [], bluetoothAlwaysPermission: BLUETOOTH, neverForLocation: true }], - "expo-web-browser", - // Local notifications only (no push server): adds Android's POST_NOTIFICATIONS; iOS asks at runtime. - "expo-notifications", - // Background checks: UIBackgroundModes "processing" + BGTaskSchedulerPermittedIdentifiers on iOS, WorkManager on Android. - "expo-background-task", - // The phone's language list (Settings → Language: "Match device"). - "expo-localization", - ], - experiments: { typedRoutes: false }, - extra: { mainnet: false }, -}; - -export default config; + }); diff --git a/apps/mobile/index.ts b/apps/mobile/index.ts index 535e3e5..7a3105e 100644 --- a/apps/mobile/index.ts +++ b/apps/mobile/index.ts @@ -1,12 +1,6 @@ +// Clip Wallet mobile is @clip-wallet/mobile-kit; this project is the brand (clip.config.ts, assets/). // Polyfills first: WalletConnect's RN compat (getRandomValues, TextEncoder, URL, Buffer…) + randomUUID. -import "./src/polyfills"; -// Background notification checks must be defined at load (expo-task-manager). -import "./src/background/background-task"; -import { createElement } from "react"; -import { registerRootComponent } from "expo"; -import { App } from "./src/App"; -import { createMobileWallet } from "./src/background/host"; +import "@clip-wallet/mobile-kit/polyfills"; +import { registerClipWallet } from "@clip-wallet/mobile-kit"; -const wallet = createMobileWallet(); - -registerRootComponent(() => createElement(App, { wallet })); +registerClipWallet({ icon: require("./assets/icon.png") }); diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js index e33ee91..8a86df5 100644 --- a/apps/mobile/metro.config.js +++ b/apps/mobile/metro.config.js @@ -1,45 +1,6 @@ -// Metro for a pnpm monorepo. Expo's default config already watches the workspace root and follows pnpm's -// symlinks; the only addition is TypeScript's ".js → .ts" import convention used by the workspace packages -// (e.g. `import "./bytes.js"` inside packages/vault/src). +// Metro for Clip Wallet mobile: Expo's defaults plus @clip-wallet/mobile-kit's wiring (the resolved clip.config as a +// virtual module, Node shims for code paths the wallet never runs, one React, workspace sources). const { getDefaultConfig } = require("expo/metro-config"); -const path = require("node:path"); +const { withClipWallet } = require("@clip-wallet/mobile-kit/metro"); -const config = getDefaultConfig(__dirname); -// Workspace packages export their TypeScript source under the "development" condition (dist/ is for npm). -config.resolver.unstable_conditionNames = ["development", ...(config.resolver.unstable_conditionNames ?? ["require", "import"])]; -const upstream = config.resolver.resolveRequest; - -/** Native modules a dependency requires on a code path the wallet never runs (see each shim). */ -const SHIMS = { - "react-native-fast-pbkdf2": path.join(__dirname, "src/shims/no-pbkdf2.js"), - // Node's crypto for hdkey (Keystone SDK → bc-ur-registry-eth), backed by @noble/hashes. - crypto: path.join(__dirname, "src/shims/node-crypto.js"), - // Node's stream for cipher-base (create-hash → bs58check → @keystonehq/bc-ur-registry): the browser build of - // readable-stream, which is what bundlers for the web use for the same package. - stream: require.resolve("readable-stream/readable-browser.js"), -}; - -config.resolver.resolveRequest = (context, moduleName, platform) => { - const resolve = upstream ?? context.resolveRequest; - if (SHIMS[moduleName]) return { type: "sourceFile", filePath: SHIMS[moduleName] }; - // One React: workspace packages with React hooks (@clip-wallet/i18n/react) have their own dev copy of - // react under pnpm; hooks from a second copy crash ("reading 'useMemo'"). Always use the app's. - if (moduleName === "react" || moduleName.startsWith("react/")) { - return resolve({ ...context, originModulePath: path.join(__dirname, "index.ts") }, moduleName, platform); - } - if (moduleName.startsWith(".") && moduleName.endsWith(".js")) { - const from = path.dirname(context.originModulePath); - if (from.includes(`${path.sep}packages${path.sep}`) && !from.includes(`${path.sep}node_modules${path.sep}`)) { - for (const ext of [".ts", ".tsx"]) { - try { - return resolve(context, moduleName.slice(0, -3) + ext, platform); - } catch { - /* try next */ - } - } - } - } - return resolve(context, moduleName, platform); -}; - -module.exports = config; +module.exports = withClipWallet(getDefaultConfig(__dirname), { configFile: "clip.config.ts", root: __dirname }); diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 68af491..469f206 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -14,33 +14,21 @@ "private": true, "main": "index.ts", "scripts": { - "inpage": "node scripts/build-inpage.mjs", - "start": "node scripts/build-inpage.mjs && expo start --dev-client", - "ios": "node scripts/build-inpage.mjs && expo run:ios", - "android": "node scripts/build-inpage.mjs && expo run:android", - "prebuild": "expo prebuild --clean", - "export:ios": "node scripts/build-inpage.mjs && expo export --platform ios --output-dir dist", + "start": "pnpm --filter @clip-wallet/mobile-kit inpage && NODE_OPTIONS=--conditions=development expo start --dev-client", + "ios": "pnpm --filter @clip-wallet/mobile-kit inpage && NODE_OPTIONS=--conditions=development expo run:ios", + "android": "pnpm --filter @clip-wallet/mobile-kit inpage && NODE_OPTIONS=--conditions=development expo run:android", + "prebuild": "NODE_OPTIONS=--conditions=development expo prebuild --clean", + "export:ios": "pnpm --filter @clip-wallet/mobile-kit inpage && NODE_OPTIONS=--conditions=development expo export --platform ios --output-dir dist", + "export:android": "pnpm --filter @clip-wallet/mobile-kit inpage && NODE_OPTIONS=--conditions=development expo export --platform android --output-dir dist", + "config": "NODE_OPTIONS=--conditions=development expo config --type public", "dapp": "node scripts/serve-test-dapp.mjs", - "typecheck": "node scripts/build-inpage.mjs && tsc -p . --noEmit", - "test": "node scripts/build-inpage.mjs && vitest run && jest" + "typecheck": "pnpm --filter @clip-wallet/mobile-kit inpage && tsc -p . --noEmit", + "test": "echo \"apps/mobile: tests live in packages/mobile-kit\"" }, "dependencies": { - "@clip-wallet/1mask": "workspace:*", "@clip-wallet/config": "workspace:*", - "@clip-wallet/core": "workspace:*", - "@clip-wallet/engine": "workspace:*", - "@clip-wallet/features": "workspace:*", - "@clip-wallet/hardware": "workspace:*", - "@clip-wallet/i18n": "workspace:*", - "@clip-wallet/link": "workspace:*", - "@clip-wallet/names": "workspace:*", - "@clip-wallet/plugins": "workspace:*", - "@clip-wallet/security": "workspace:*", - "@clip-wallet/social": "workspace:*", - "@clip-wallet/ui": "workspace:*", - "@clip-wallet/vault": "workspace:*", + "@clip-wallet/mobile-kit": "workspace:*", "@ledgerhq/react-native-hw-transport-ble": "6.41.0", - "@noble/hashes": "2.4.0", "@react-native-async-storage/async-storage": "2.2.0", "@react-native-community/netinfo": "12.0.1", "@walletconnect/react-native-compat": "2.25.0", @@ -61,8 +49,6 @@ "expo-status-bar": "~57.0.1", "expo-task-manager": "~57.0.21", "expo-web-browser": "~57.0.3", - "pako": "2.2.0", - "qrcode": "^1.5.4", "react": "19.2.3", "react-native": "0.86.3", "react-native-argon2": "4.0.0", @@ -71,22 +57,11 @@ "react-native-passkey": "3.6.2", "react-native-safe-area-context": "~5.7.0", "react-native-svg": "15.15.4", - "react-native-webview": "13.16.1", - "readable-stream": "^3.6.2", - "ses": "2.3.0" + "react-native-webview": "13.16.1" }, "devDependencies": { - "@react-native/jest-preset": "0.86.3", - "@testing-library/react-native": "^13.3.3", - "@types/jest": "^29.5.14", - "@types/qrcode": "^1.5.6", + "@types/node": "^22.20.5", "@types/react": "~19.2.0", - "esbuild": "^0.25.0", - "happy-dom": "^20.14.5", - "jest": "^29.7.0", - "jest-expo": "~57.0.5", - "react-test-renderer": "19.2.3", - "typescript": "^5.9.3", - "vitest": "~4.1.11" + "typescript": "^5.9.3" } } diff --git a/apps/mobile/src/env.ts b/apps/mobile/src/env.ts deleted file mode 100644 index 8911bb6..0000000 --- a/apps/mobile/src/env.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Build-time settings. EXPO_PUBLIC_* values are inlined by Expo's bundler at build time; they are public - * identifiers, never secrets. Nothing here is logged. - */ -import clipConfig from "../clip.config"; - -const wcProjectId = process.env.EXPO_PUBLIC_WC_PROJECT_ID?.trim() || undefined; -const passkeyRpId = process.env.EXPO_PUBLIC_PASSKEY_RP_ID?.trim() || undefined; - -export const APP = { - config: { ...clipConfig, walletConnect: { ...clipConfig.walletConnect, projectId: wcProjectId } }, - /** Reown project id; WalletConnect is switched off (and the UI says so) without it. */ - wcProjectId, - /** WebAuthn relying party for real passkeys (needs webcredentials association). */ - passkeyRpId, - /** Placeholder metadata URL shown to WalletConnect peers until the app has a site. */ - siteUrl: "https://clipwallet.example", - iconUrl: "https://clipwallet.example/icon.png", - /** EIP-6963 / Wallet Standard identity (icon must be a data URI). */ - identity: { - name: clipConfig.name, - rdns: clipConfig.rdns, - icon: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMTI4IDEyOCI+CiAgPHRpdGxlPkNsaXAgV2FsbGV0PC90aXRsZT4KICA8cmVjdCB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgcng9IjMwIiBmaWxsPSIjRkYzQzAwIi8+CiAgPGcgZmlsbD0iI0ZGRkZGRiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDY0IDY0KSBza2V3WCgtNikgdHJhbnNsYXRlKC02NCAtNjQpIj48cGF0aCBkPSJNNzIgMThDNDYgMzAgMzIgNTAgMzIgNzJjMCA3IDEgMTIgMyAxNmgzN3oiLz48cGF0aCBkPSJNODQgNDBjMTAgMTIgMTQgMjYgMTMgNDAtNCA0LTkgNy0xMyA4eiIvPjxwYXRoIGQ9Ik0yNCA5OWg4MmMtNiA3LTE1IDExLTI2IDExSDQ4Yy0xMSAwLTE5LTQtMjQtMTF6Ii8+PC9nPgo8L3N2Zz4K", - }, - /** Deep links: clipwallet://wc?uri=…, clipwallet://browse?url=… ; universal links once a domain is associated. */ - scheme: "clipwallet", - /** Secure Trade share links: https:///trade#offer=… (universal link) or clipwallet://trade#offer=…. */ - tradeLinkBase: process.env.CLIP_ASSOCIATED_DOMAIN ? `https://${process.env.CLIP_ASSOCIATED_DOMAIN}/trade` : "clipwallet://trade", - /** - * Google / Apple sign-in for passkey backups returns here (an https universal link on the associated domain, - * listed in the backup service's OIDC_RETURN_URLS). Unset = the buttons stay hidden; email sign-in still works. - */ - backupReturnUrl: process.env.CLIP_ASSOCIATED_DOMAIN ? `https://${process.env.CLIP_ASSOCIATED_DOMAIN}/backup` : undefined, -} as const; diff --git a/apps/mobile/tsconfig.json b/apps/mobile/tsconfig.json index ed2b749..a6b712c 100644 --- a/apps/mobile/tsconfig.json +++ b/apps/mobile/tsconfig.json @@ -4,12 +4,11 @@ "strict": true, "noUncheckedIndexedAccess": true, "verbatimModuleSyntax": false, - // Type-check workspace packages against their TypeScript source ("development"), not the react-native - // condition. Metro still bundles the "react-native" export condition at runtime (see - // docs/phase2/integration/mobile.md, Hedera note). + // Type-check workspace packages against their TypeScript source ("development"). "customConditions": ["development"], - "types": ["jest"] + "types": ["node"] }, - "include": ["index.ts", "app.config.ts", "clip.config.ts", "src", "test", "scripts/*.d.ts"], - "exclude": ["node_modules", "dist", "ios", "android", "test/**/*.vitest.ts"] + // The kit's ambient declarations (pako) for its TypeScript source. + "include": ["index.ts", "app.config.ts", "clip.config.ts", "../../packages/mobile-kit/src/**/*.d.ts"], + "exclude": ["node_modules", "dist", "ios", "android"] } diff --git a/apps/mobile/vitest.config.ts b/apps/mobile/vitest.config.ts deleted file mode 100644 index f0f2360..0000000 --- a/apps/mobile/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from "vitest/config"; - -// Node-side tests (WebView bridge end-to-end against the real injected bundle, KDF adapter). Screens use jest-expo. -export default defineConfig({ - test: { include: ["test/**/*.vitest.ts"], environment: "node" }, -}); diff --git a/packages/config/package.json b/packages/config/package.json index 7687c79..3fe3412 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -27,6 +27,12 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js", "default": "./dist/index.js" + }, + "./node": { + "development": "./src/node.ts", + "types": "./dist/node.d.ts", + "import": "./dist/node.js", + "default": "./dist/node.js" } }, "files": [ @@ -43,6 +49,11 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js", "default": "./dist/index.js" + }, + "./node": { + "types": "./dist/node.d.ts", + "import": "./dist/node.js", + "default": "./dist/node.js" } } }, diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 959aec4..62d15a7 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -38,6 +38,9 @@ export const NETWORK_FAMILIES = [ export const ROUTE_MODES = ["balanced", "cheapest", "fastest", "reliable", "greenest"] as const; export const TRUST_TIERS = ["attested", "committee", "light-client", "validity-proof"] as const; export const HARDWARE = ["ledger", "keystone"] as const; +/** The languages the wallet ships (BCP 47), same order as @clip-wallet/i18n LOCALE_CODES (config has no runtime dependency on i18n). */ +export const LANGUAGES = ["en", "de", "fr", "es", "pt-BR", "it", "tr", "ja", "ko", "zh-Hans", "ar", "hi"] as const; +export type Language = (typeof LANGUAGES)[number]; /** "evm:*", "evm:8453", "evm:base-sepolia", or a non-EVM family name ("hedera", "solana", "cardano", …). */ const NETWORK_PATTERN = new RegExp(`^(?:evm:(?:\\*|[1-9]\\d*|[a-z][a-z0-9-]*)|${NETWORK_FAMILIES.filter((f) => f !== "evm").join("|")})$`); @@ -49,6 +52,12 @@ const NETWORK_MESSAGE = `use "evm:*", "evm:", ${NETWORK_FAMILIES.filte const HTTPS_BASE = /^https:\/\/[a-z0-9.-]+(?::\d+)?(?:\/[\w.~-]+)*$/; const HEX_COLOR = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; const RDNS = /^[a-z][a-z0-9-]*(?:\.[a-z0-9-]+)+$/; +/** Application / bundle ids: reverse-domain, letters, digits and hyphens, at least two parts (com.example.mywallet). */ +const APP_ID = /^[a-zA-Z][a-zA-Z0-9-]*(?:\.[a-zA-Z0-9-]+)+$/; +/** Android application ids: each part starts with a letter, then letters, digits and underscores (no hyphens). */ +const ANDROID_PACKAGE = /^[a-zA-Z][a-zA-Z0-9_]*(?:\.[a-zA-Z][a-zA-Z0-9_]*)+$/; +/** A URL scheme for deep links (RFC 3986 scheme, lower case, 2–32 characters). */ +const SCHEME = /^[a-z][a-z0-9+.-]{1,31}$/; function env(name: string): string | undefined { const v = (globalThis as { process?: { env?: Record } }).process?.env?.[name]; @@ -130,6 +139,14 @@ const mainnet = z.unknown().optional().transform((v, ctx): MainnetSetting => { return z.NEVER; }); +/** A hosted-mode block: { enabled } plus whatever Clip Cloud's spec adds (kept as given, never acted on here). */ +function hostedSwitch() { + return z + .object({ enabled: z.boolean("use true or false").default(false) }) + .loose() + .default({ enabled: false }); +} + export const clipConfigSchema = z .object({ name: z.string("give your wallet a name").trim().min(1, "give your wallet a name").max(40, "keep the name to 40 characters or fewer"), @@ -217,6 +234,45 @@ export const clipConfigSchema = z }) .strict() .default({}), + /** + * Languages the wallet offers in Settings → Language (and matches the device against). Default: all twelve. + * The first one listed is the fallback when the device's language isn't offered. + */ + languages: z + .array(z.enum(LANGUAGES, `use the languages the wallet ships: ${LANGUAGES.join(", ")}`)) + .min(1, "offer at least one language") + .refine((l) => new Set(l).size === l.length, "each language is listed once") + .default([...LANGUAGES]), + /** + * The desktop and mobile apps' id (reverse domain, like com.example.mywallet). Default: rdns. Platform ids derive + * from it (platformIds()): iOS bundle id and Android package = appId, desktop = appId + ".desktop"; override them + * one by one under desktop / mobile. + */ + appId: z.string().regex(APP_ID, "use a reverse-domain id like com.example.mywallet (letters, digits, hyphens)").optional(), + /** Deep links (://wc?uri=…, ://browse?url=…). Default: the wallet key, e.g. "acmewallet". */ + scheme: z.string().regex(SCHEME, "use a lower-case URL scheme like acmewallet (letters, digits, + . -; 2 to 32 characters)").optional(), + /** Desktop app (Electron) overrides. */ + desktop: z + .object({ appId: z.string().regex(APP_ID, "use a reverse-domain id like com.example.mywallet.desktop").optional() }) + .strict() + .default({}), + /** Mobile app (Expo) overrides. */ + mobile: z + .object({ + bundleId: z.string().regex(APP_ID, "use a reverse-domain iOS bundle id like com.example.mywallet").optional(), + androidPackage: z + .string() + .regex(ANDROID_PACKAGE, "use an Android package like com.example.mywallet (each part starts with a letter; letters, digits and _ only)") + .optional(), + }) + .strict() + .default({}), + /** + * Reserved for Clip Cloud's hosted mode. Accepted so a hosted config validates here too; the open-source kit never + * collects fees or reports usage, whatever these say. Default: off. + */ + fees: hostedSwitch(), + usage: hostedSwitch(), mainnet, }) .strict(); @@ -330,3 +386,76 @@ export function mainnetProblems(config: ClipConfig, env: Record "acme-wallet": file-safe, for executables, packages and artifact names. */ +export function slugOfName(name: string): string { + return ( + name + .toLowerCase() + .normalize("NFKD") + .replace(/[̀-ͯ]/g, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") || "wallet" + ); +} + +/** An Android package from any reverse-domain id: hyphens become underscores, parts that start with a digit get a prefix. */ +export function androidPackageOf(id: string): string { + return id + .split(".") + .map((p) => p.replace(/-/g, "_").replace(/[^A-Za-z0-9_]/g, "")) + .map((p) => (/^[A-Za-z]/.test(p) ? p : `x${p}`)) + .join("."); +} + +/** Every id a wallet's platforms use, from clip.config (pure; the kits and create-clip-wallet share it). */ +export interface PlatformIds { + /** The wallet's display name on every platform. */ + name: string; + /** Base application id (appId, else rdns). */ + appId: string; + /** Deep-link scheme (scheme, else the wallet key). */ + scheme: string; + extension: { /** Firefox add-on id. */ geckoId: string }; + desktop: { + /** electron-builder appId: macOS CFBundleIdentifier, Windows AppUserModelID. */ + appId: string; + productName: string; + /** Linux executable, deb package and file-safe name ("acme-wallet"). */ + executableName: string; + /** electron-builder artifactName pattern ("Acme-Wallet-${version}-${os}-${arch}.${ext}"). */ + artifactName: string; + }; + ios: { bundleIdentifier: string }; + android: { package: string }; + /** Expo slug ("acme-wallet"). */ + slug: string; +} + +export function platformIds(config: Pick): PlatformIds { + const appId = config.appId ?? config.rdns; + const slug = slugOfName(config.name); + const fileName = config.name.replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "Wallet"; + return { + name: config.name, + appId, + scheme: config.scheme ?? walletKey(config), + extension: { geckoId: `wallet@${config.rdns.split(".").reverse().join(".")}` }, + desktop: { + appId: config.desktop?.appId ?? `${appId}.desktop`, + productName: config.name, + executableName: slug, + artifactName: `${fileName}-\${version}-\${os}-\${arch}.\${ext}`, + }, + ios: { bundleIdentifier: config.mobile?.bundleId ?? appId }, + android: { package: config.mobile?.androidPackage ?? androidPackageOf(appId) }, + slug, + }; +} + +/** The languages the wallet offers, in config order (all twelve unless clip.config narrows them). */ +export function enabledLanguages(config: Partial>): Language[] { + return config.languages?.length ? [...config.languages] : [...LANGUAGES]; +} diff --git a/packages/config/src/node.ts b/packages/config/src/node.ts new file mode 100644 index 0000000..31bc221 --- /dev/null +++ b/packages/config/src/node.ts @@ -0,0 +1,123 @@ +/** + * Node-only helpers for build tools that need clip.config.ts outside a bundler: Expo's app.config.ts, + * electron-builder's config file, create-clip-wallet. "@clip-wallet/config/node"; the main entry stays environment-free. + */ +import { execFileSync } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import { extname, join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { + ConfigError, + WALLETCONNECT_ENV, + defineConfig, + isMainnetEnabled, + mainnetProblems, + validateConfig, + type ClipConfig, + type ClipConfigInput, +} from "@clip-wallet/config"; + +/** Node flags needed to import a .ts file: type stripping is on by default from Node 22.18 / 23.6. */ +export function typeStrippingFlags(version: string = process.versions.node): string[] { + const [major = 0, minor = 0] = version.split(".").map(Number); + if (major > 23 || (major === 23 && minor >= 6) || (major === 22 && minor >= 18)) return []; + if (major === 22 && minor >= 6) return ["--experimental-strip-types"]; + throw new Error(`Reading clip.config.ts needs Node 22.18 or newer (this is ${version}).`); +} + +/** Is this Node process resolving workspace packages from source (--conditions=development, the Clip Wallet monorepo)? */ +function devConditions(): boolean { + const flags = [...process.execArgv, ...(process.env.NODE_OPTIONS ?? "").split(/\s+/)]; + return flags.some((f, i) => /^(?:--conditions|-C)=development$/.test(f) || ((f === "--conditions" || f === "-C") && flags[i + 1] === "development")); +} + +/** + * Evaluate clip.config.ts (its default export, already run through defineConfig) in a child Node process and return + * the validated config. Synchronous, so CommonJS config files (app.config.ts under Expo, electron-builder) can use it. + * Problems come back as a ConfigError in plain words. + */ +export function loadClipConfigSync(file: string, options: { cwd?: string; env?: NodeJS.ProcessEnv } = {}): ClipConfig { + const path = resolve(options.cwd ?? process.cwd(), file); + if (!existsSync(path)) throw new ConfigError([`clip.config.ts isn't at ${path}`]); + const code = `const m = await import(${JSON.stringify(pathToFileURL(path).href)}); process.stdout.write(JSON.stringify(m.default ?? null));`; + const flags = [...typeStrippingFlags(), ...(devConditions() && !(process.env.NODE_OPTIONS ?? "").includes("development") ? ["--conditions=development"] : [])]; + let out: string; + try { + out = execFileSync(process.execPath, [...flags, "--no-warnings", "--input-type=module", "-e", code], { + env: options.env ?? process.env, + stdio: ["ignore", "pipe", "pipe"], + encoding: "utf8", + }); + } catch (e) { + const stderr = String((e as { stderr?: string }).stderr ?? ""); + // defineConfig's ConfigError lists the problems one per line (" - setting: problem"). + const problems = [...stderr.matchAll(/^ {2}- (.+)$/gm)].map((m) => String(m[1])); + if (problems.length) throw new ConfigError(problems); + throw new Error(`Couldn't read ${path}:\n${stderr.trim().split("\n").slice(0, 12).join("\n")}`); + } + const r = validateConfig(JSON.parse(out)); + if (!r.ok) throw new ConfigError(r.problems); + return r.config; +} + +/** CLIP_* lines from a .env file (KEY=value, # comments). Other keys are ignored; values are never logged. */ +export function readClipEnv(file: string): Record { + if (!existsSync(file)) return {}; + const out: Record = {}; + for (const line of readFileSync(file, "utf8").split(/\r?\n/)) { + const m = /^\s*(CLIP_[A-Z0-9_]+)\s*=\s*(.*?)\s*$/.exec(line); + if (m && m[2] !== "") out[m[1]!] = m[2]!.replace(/^(['"])(.*)\1$/, "$2"); + } + return out; +} + +/** + * The build environment for a platform project: CLIP_* values from `.env` in each directory (earlier directories + * win: the platform's own, then the wallet project's), with the process environment on top. + */ +export function buildEnv(dirs: readonly string[], env: Record = process.env): Record { + const files = [...dirs].reverse().map((d) => readClipEnv(join(d, ".env"))); + return Object.assign({}, ...files, env); +} + +/** Open boxes ("- [ ] …") in MAINNET.md in `dir` (the checklist next to clip.config.ts); none when there is no file. */ +export function openChecklistItems(dir: string): string[] { + const file = join(dir, "MAINNET.md"); + if (!existsSync(file)) return []; + return readFileSync(file, "utf8") + .split(/\r?\n/) + .map((l) => /^\s*- \[ \] (.+)$/.exec(l)?.[1]) + .filter((x): x is string => !!x) + .map((item) => `MAINNET.md: ${item.replace(/`/g, "")}`); +} + +/** + * Re-validate a config with the build environment applied (the WalletConnect project id from + * CLIP_WALLETCONNECT_PROJECT_ID, or `fallbackWcEnv`) and enforce the mainnet checklist: a mainnet build is refused while + * mainnetProblems() lists anything or MAINNET.md (in `checklistDir`) has an open box. Throws ConfigError in plain words. + */ +export function resolveBuildConfig( + config: ClipConfig, + env: Record, + options: { checklistDir?: string; fallbackWcEnv?: string } = {}, +): ClipConfig { + const projectId = config.walletConnect.projectId ?? (env[WALLETCONNECT_ENV] || (options.fallbackWcEnv ? env[options.fallbackWcEnv] : undefined) || undefined); + const resolved = defineConfig({ ...(config as ClipConfigInput), walletConnect: projectId ? { projectId } : {} }); + if (isMainnetEnabled(resolved)) { + const problems = [...mainnetProblems(resolved, env), ...(options.checklistDir ? openChecklistItems(options.checklistDir) : [])]; + if (problems.length) throw new ConfigError(problems.map((p) => `mainnet checklist: ${p}`)); + } + return resolved; +} + +/** data: URI for the icon clip.config names (svg or png, relative to `dir`, the folder clip.config.ts is in). */ +export function iconDataUri(dir: string, icon: string): `data:image/${"svg+xml" | "png"};base64,${string}` { + if (icon.startsWith("data:image/")) return icon as never; + if (/^https?:\/\//.test(icon)) { + throw new ConfigError([`icon: ship the icon with the wallet (./icon.svg or ./icon.png); ${icon} would be fetched from someone else's server`]); + } + const file = resolve(dir, icon); + if (!existsSync(file)) throw new ConfigError([`icon: ${icon} isn't there; put the icon next to clip.config.ts`]); + const type = extname(file).toLowerCase() === ".png" ? "png" : "svg+xml"; + return `data:image/${type};base64,${readFileSync(file).toString("base64")}`; +} diff --git a/packages/config/test/config.test.ts b/packages/config/test/config.test.ts index e56ff26..7885099 100644 --- a/packages/config/test/config.test.ts +++ b/packages/config/test/config.test.ts @@ -11,7 +11,12 @@ import { includesEvmChain, isMainnetEnabled, isPlaceholderRdns, + LANGUAGES, + androidPackageOf, + enabledLanguages, mainnetProblems, + platformIds, + slugOfName, rdnsDomain, validateConfig, walletKey, @@ -44,6 +49,11 @@ describe("defaults", () => { walletConnect: {}, passkeys: { enabled: true }, services: {}, + languages: [...LANGUAGES], + desktop: {}, + mobile: {}, + fees: { enabled: false }, + usage: { enabled: false }, mainnet: false, }); expect(isMainnetEnabled(c)).toBe(false); @@ -191,3 +201,90 @@ describe("mainnet checklist", () => { expect(mainnetProblems(defineConfig({ name: "Acme", rdns: "com.acme.wallet", homepage: "https://acme.example", extension: { key: "A".repeat(392) }, walletConnect: { projectId: "0".repeat(32) }, mainnet: ack }))).toEqual([]); }); }); + +describe("languages, ids and deep links", () => { + it("offers all twelve languages unless narrowed, in config order", () => { + expect(enabledLanguages(defineConfig(base))).toEqual([...LANGUAGES]); + expect(defineConfig({ ...base, languages: ["de", "en"] }).languages).toEqual(["de", "en"]); + expect(problems({ ...base, languages: [] })).toEqual(["languages: offer at least one language"]); + expect(problems({ ...base, languages: ["en", "en"] })).toEqual(["languages: each language is listed once"]); + expect(problems({ ...base, languages: ["xx"] })[0]).toMatch(/^languages\.0: use the languages the wallet ships: en, de/); + }); + + it("matches @clip-wallet/i18n's shipped locales", async () => { + const { LOCALE_CODES } = await import("../../i18n/src/locales.js"); + expect([...LANGUAGES]).toEqual([...LOCALE_CODES]); + }); + + it("derives every platform id from rdns, or appId, with per-platform overrides", () => { + expect(platformIds(defineConfig({ name: "Acme Wallet", rdns: "com.acme.wallet" }))).toEqual({ + name: "Acme Wallet", + appId: "com.acme.wallet", + scheme: "acmewallet", + extension: { geckoId: "wallet@wallet.acme.com" }, + desktop: { appId: "com.acme.wallet.desktop", productName: "Acme Wallet", executableName: "acme-wallet", artifactName: "Acme-Wallet-${version}-${os}-${arch}.${ext}" }, + ios: { bundleIdentifier: "com.acme.wallet" }, + android: { package: "com.acme.wallet" }, + slug: "acme-wallet", + }); + const ids = platformIds( + defineConfig({ name: "Ünï Wallet 2", rdns: "com.acme.wallet", appId: "com.example.my-wallet", scheme: "uni", desktop: { appId: "com.example.desk" }, mobile: { bundleId: "com.example.ios" } }), + ); + expect(ids).toMatchObject({ appId: "com.example.my-wallet", scheme: "uni", desktop: { appId: "com.example.desk", executableName: "uni-wallet-2" }, ios: { bundleIdentifier: "com.example.ios" }, android: { package: "com.example.my_wallet" } }); + // Clip Wallet's own ids stay what its apps shipped with. + const clip = platformIds(defineConfig({ name: "Clip Wallet", rdns: "org.coldai.clipwallet" })); + expect([clip.scheme, clip.desktop.appId, clip.ios.bundleIdentifier, clip.android.package]).toEqual(["clipwallet", "org.coldai.clipwallet.desktop", "org.coldai.clipwallet", "org.coldai.clipwallet"]); + }); + + it("makes any reverse domain a valid Android package and any name a file-safe slug", () => { + expect(androidPackageOf("com.9lives.my-wallet")).toBe("com.x9lives.my_wallet"); + expect(slugOfName(" Ácme Wallet!! ")).toBe("acme-wallet"); + expect(slugOfName("日本")).toBe("wallet"); + }); + + it("checks ids and schemes in plain words", () => { + expect(problems({ ...base, appId: "mywallet" })).toEqual(["appId: use a reverse-domain id like com.example.mywallet (letters, digits, hyphens)"]); + expect(problems({ ...base, scheme: "Acme Wallet" })[0]).toMatch(/^scheme: use a lower-case URL scheme/); + expect(problems({ ...base, mobile: { androidPackage: "com.my-wallet" } })[0]).toMatch(/^mobile\.androidPackage: use an Android package/); + }); +}); + +describe("hosted mode (reserved for Clip Cloud)", () => { + it("accepts fees and usage, off by default, and keeps fields it doesn't know", () => { + const c = defineConfig({ ...base, fees: { enabled: true, bps: 25 } as never, usage: {} }); + expect(c.fees).toEqual({ enabled: true, bps: 25 }); + expect(c.usage).toEqual({ enabled: false }); + expect(problems({ ...base, fees: { enabled: "yes" } })).toEqual(["fees.enabled: use true or false"]); + }); +}); + +describe("loadClipConfigSync (@clip-wallet/config/node)", () => { + it("evaluates a clip.config.ts with its JSON identity in a child Node and validates it", async () => { + const { mkdtempSync, mkdirSync, symlinkSync, writeFileSync } = await import("node:fs"); + const { tmpdir } = await import("node:os"); + const { join, resolve } = await import("node:path"); + const { loadClipConfigSync, typeStrippingFlags } = await import("../src/node.js"); + const dir = mkdtempSync(join(tmpdir(), "clip-config-")); + mkdirSync(join(dir, "node_modules", "@clip-wallet"), { recursive: true }); + symlinkSync(resolve(__dirname, ".."), join(dir, "node_modules", "@clip-wallet", "config")); + writeFileSync(join(dir, "wallet.identity.json"), JSON.stringify({ name: "Acme Wallet", rdns: "com.acme.wallet" })); + writeFileSync( + join(dir, "clip.config.ts"), + 'import { defineConfig } from "@clip-wallet/config";\nimport identity from "./wallet.identity.json" with { type: "json" };\nconst langs: ("en" | "de")[] = ["de", "en"];\nexport default defineConfig({ ...identity, languages: langs });\n', + ); + const prev = process.env.NODE_OPTIONS; + process.env.NODE_OPTIONS = `${prev ?? ""} --conditions=development`.trim(); + try { + const c = loadClipConfigSync("clip.config.ts", { cwd: dir }); + expect([c.name, c.rdns, c.languages]).toEqual(["Acme Wallet", "com.acme.wallet", ["de", "en"]]); + writeFileSync(join(dir, "clip.config.ts"), 'import { defineConfig } from "@clip-wallet/config";\nexport default defineConfig({ name: "", rdns: "x" } as never);\n'); + expect(() => loadClipConfigSync(join(dir, "clip.config.ts"))).toThrow(/name: give your wallet a name/); + } finally { + if (prev === undefined) delete process.env.NODE_OPTIONS; + else process.env.NODE_OPTIONS = prev; + } + expect(typeStrippingFlags("22.12.0")).toEqual(["--experimental-strip-types"]); + expect(typeStrippingFlags("24.1.0")).toEqual([]); + expect(() => typeStrippingFlags("20.19.0")).toThrow(/Node 22.18/); + }, 30_000); +}); diff --git a/packages/desktop-kit/.gitignore b/packages/desktop-kit/.gitignore new file mode 100644 index 0000000..0dfed8a --- /dev/null +++ b/packages/desktop-kit/.gitignore @@ -0,0 +1,3 @@ +dist/ +# Built from @clip-wallet/1mask at build/typecheck/test time. +src/preload/inpage.generated.ts diff --git a/packages/desktop-kit/LICENSE b/packages/desktop-kit/LICENSE new file mode 100644 index 0000000..fd2e97a --- /dev/null +++ b/packages/desktop-kit/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 ColdAI (https://coldai.org) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/desktop-kit/NOTICE b/packages/desktop-kit/NOTICE new file mode 100644 index 0000000..f33c93f --- /dev/null +++ b/packages/desktop-kit/NOTICE @@ -0,0 +1,8 @@ +Clip Wallet: @clip-wallet/desktop-kit +Copyright 2026 ColdAI (https://coldai.org) + +This product includes software developed by ColdAI (https://coldai.org). +Licensed under the Apache License, Version 2.0 (see LICENSE). + +"Clip Wallet", "1Mask" and the Clip Wallet logo are trademarks of ColdAI. The Apache +License 2.0 grants no rights to use them (Section 6). diff --git a/packages/desktop-kit/README.md b/packages/desktop-kit/README.md new file mode 100644 index 0000000..fac2c3e --- /dev/null +++ b/packages/desktop-kit/README.md @@ -0,0 +1,65 @@ +# @clip-wallet/desktop-kit + +The Clip Wallet desktop app (Electron: macOS, Windows, Linux) as a library. A wallet project keeps its identity +(`clip.config.ts`, icons), `electron.vite.config.ts`, `electron-builder.config.cjs` and one-line entrypoints; the main +process (vault, engine, approvals, Touch ID, Ledger over WebHID, linked devices), the sandboxed preloads, the built-in +dapp browser with 1Mask for all 14 network families and the pages come from here, versioned and signed. + +```ts +// electron.vite.config.ts +import { defineConfig } from "electron-vite"; +import { clipDesktop } from "@clip-wallet/desktop-kit/electron-vite"; +import clipConfig from "../../clip.config"; + +export default defineConfig(clipDesktop({ config: clipConfig, root: import.meta.dirname, configDir: "../.." })); +``` + +```js +// electron-builder.config.cjs +const { electronBuilderConfig } = require("@clip-wallet/desktop-kit/builder"); +module.exports = electronBuilderConfig({ configFile: "../../clip.config.ts", root: __dirname }); +``` + +| Entrypoint | File in the project | Import | +| --- | --- | --- | +| main process | `src/main/index.ts` | `import "@clip-wallet/desktop-kit/main"` | +| wallet / approval / toolbar preload | `src/preload/wallet.ts` | `import "@clip-wallet/desktop-kit/preload/wallet"` | +| dapp-tab preload (1Mask) | `src/preload/dapp.ts` | `import "@clip-wallet/desktop-kit/preload/dapp"` | +| wallet window | `src/renderer/wallet/main.tsx` | `mountWallet()` from `/renderer` | +| approval window | `src/renderer/approval/main.tsx` | `mountApproval()` from `/renderer` | +| browser toolbar | `src/renderer/browser/main.tsx` | `mountBrowserChrome()` from `/renderer` | + +The pages' `index.html` use `%CLIP_WALLET_NAME%` and `%CLIP_WALLET_CSP%`, which the build fills in. +`npx create-clip-wallet my-wallet` writes all of this for you (`packages/desktop`). + +## What the kit takes from clip.config.ts + +- **Identity.** Name (window titles, menus, tray, permission prompts), the EIP-6963 / Wallet Standard identity dapps + see in the built-in browser (name, rdns, the icon inlined), the TON Connect key, app id (`platformIds(config).desktop.appId`, + default `.desktop`), deep-link scheme (`://wc?uri=…`), product, executable and artifact names. +- **Theme, networks, languages, services.** The same tokens, network list, language list and optional hosted services + as the extension and the phone. +- **Build environment.** `CLIP_WALLETCONNECT_PROJECT_ID` (or `CLIP_WC_PROJECT_ID`), `CLIP_UPDATES=1`, + `CLIP_EXTENSION_IDS`, from the environment or `CLIP_*` lines in `.env` (the project's, then the wallet root's). +- **Mainnet checklist.** A config with mainnet enabled is refused while `mainnetProblems()` lists anything or MAINNET.md + next to clip.config.ts has an open box. The security floor (open phishing lists) has no switch. + +## Packaging and signing + +`electronBuilderConfig()` targets dmg + zip (macOS, arm64 and x64), NSIS + zip (Windows), AppImage + deb + tar.gz +(Linux), with the icons create-clip-wallet renders from the logo (`build/icon.icns`, `build/icon.ico`, `build/icons/`). +Unsigned unless electron-builder's signing variables are set: `CSC_LINK` / `CSC_KEY_PASSWORD` or `CSC_NAME` (Apple +Developer ID Application), `APPLE_API_KEY` / `APPLE_API_KEY_ID` / `APPLE_API_ISSUER` or `APPLE_ID` / +`APPLE_APP_SPECIFIC_PASSWORD` / `APPLE_TEAM_ID` (notarization), `WIN_CSC_LINK` / `WIN_CSC_KEY_PASSWORD` (Windows). + +Peer dependencies: `electron`, `electron-vite`, `vite`, `react`, `react-dom`. Node 22.18 or newer for the build helpers. + +## Versioning and provenance + +Published from [ColdAI-org/clip-wallet](https://github.com/ColdAI-org/clip-wallet) by CI with npm provenance: every +tarball is signed and traceable to the commit that built it (`npm audit signatures` checks it). All `@clip-wallet/*` +packages share one version; pin it exactly. Pre-release: test networks by default. + +## Licence + +Apache-2.0: see [LICENSE](./LICENSE) and [NOTICE](./NOTICE). "Clip Wallet" and "1Mask" are ColdAI trademarks (not licensed). diff --git a/packages/desktop-kit/package.json b/packages/desktop-kit/package.json new file mode 100644 index 0000000..57dcf91 --- /dev/null +++ b/packages/desktop-kit/package.json @@ -0,0 +1,203 @@ +{ + "name": "@clip-wallet/desktop-kit", + "version": "0.1.0", + "description": "Build a branded Clip Wallet desktop app (Electron: macOS, Windows, Linux): main process, preloads, pages, built-in dapp browser with 1Mask, configured by clip.config.ts", + "keywords": [ + "clip-wallet", + "wallet", + "web3" + ], + "license": "Apache-2.0", + "author": "ColdAI (https://coldai.org)", + "homepage": "https://github.com/ColdAI-org/clip-wallet/tree/main/packages/desktop-kit#readme", + "bugs": { + "url": "https://github.com/ColdAI-org/clip-wallet/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ColdAI-org/clip-wallet.git", + "directory": "packages/desktop-kit" + }, + "type": "module", + "sideEffects": [ + "*.css", + "./src/main/index.ts", + "./src/preload/*.ts", + "./src/native-host/main.ts", + "./dist/main/index.js", + "./dist/preload/*.js", + "./dist/native-host/main.js" + ], + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./electron-vite": { + "development": "./src/electron-vite.ts", + "types": "./dist/electron-vite.d.ts", + "import": "./dist/electron-vite.js", + "default": "./dist/electron-vite.js" + }, + "./builder": { + "development": "./src/builder.ts", + "types": "./dist/builder.d.ts", + "import": "./dist/builder.js", + "default": "./dist/builder.js" + }, + "./main": { + "development": "./src/main/index.ts", + "types": "./dist/main/index.d.ts", + "import": "./dist/main/index.js", + "default": "./dist/main/index.js" + }, + "./preload/wallet": { + "development": "./src/preload/wallet.ts", + "types": "./dist/preload/wallet.d.ts", + "import": "./dist/preload/wallet.js", + "default": "./dist/preload/wallet.js" + }, + "./preload/dapp": { + "development": "./src/preload/dapp.ts", + "types": "./dist/preload/dapp.d.ts", + "import": "./dist/preload/dapp.js", + "default": "./dist/preload/dapp.js" + }, + "./renderer": { + "development": "./src/renderer/index.ts", + "types": "./dist/renderer/index.d.ts", + "import": "./dist/renderer/index.js", + "default": "./dist/renderer/index.js" + }, + "./native-host": { + "development": "./src/native-host/main.ts", + "types": "./dist/native-host/main.d.ts", + "import": "./dist/native-host/main.js", + "default": "./dist/native-host/main.js" + }, + "./browser.css": { + "development": "./src/renderer/browser/browser.css", + "default": "./dist/renderer/browser/browser.css" + }, + "./csp": { + "development": "./src/csp.ts", + "types": "./dist/csp.d.ts", + "import": "./dist/csp.js", + "default": "./dist/csp.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "NOTICE" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./electron-vite": { + "types": "./dist/electron-vite.d.ts", + "import": "./dist/electron-vite.js", + "default": "./dist/electron-vite.js" + }, + "./builder": { + "types": "./dist/builder.d.ts", + "import": "./dist/builder.js", + "default": "./dist/builder.js" + }, + "./main": { + "types": "./dist/main/index.d.ts", + "import": "./dist/main/index.js", + "default": "./dist/main/index.js" + }, + "./preload/wallet": { + "types": "./dist/preload/wallet.d.ts", + "import": "./dist/preload/wallet.js", + "default": "./dist/preload/wallet.js" + }, + "./preload/dapp": { + "types": "./dist/preload/dapp.d.ts", + "import": "./dist/preload/dapp.js", + "default": "./dist/preload/dapp.js" + }, + "./renderer": { + "types": "./dist/renderer/index.d.ts", + "import": "./dist/renderer/index.js", + "default": "./dist/renderer/index.js" + }, + "./native-host": { + "types": "./dist/native-host/main.d.ts", + "import": "./dist/native-host/main.js", + "default": "./dist/native-host/main.js" + }, + "./browser.css": "./dist/renderer/browser/browser.css", + "./csp": { + "types": "./dist/csp.d.ts", + "import": "./dist/csp.js", + "default": "./dist/csp.js" + } + } + }, + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "node scripts/build-inpage.mjs && node ../../tools/release/build-package.mjs", + "inpage": "node scripts/build-inpage.mjs", + "typecheck": "node scripts/build-inpage.mjs && tsc -p . --noEmit", + "test": "node scripts/build-inpage.mjs && vitest run" + }, + "dependencies": { + "@clip-wallet/1mask": "workspace:*", + "@clip-wallet/chains-ton": "workspace:*", + "@clip-wallet/config": "workspace:*", + "@clip-wallet/core": "workspace:*", + "@clip-wallet/engine": "workspace:*", + "@clip-wallet/features": "workspace:*", + "@clip-wallet/hardware": "workspace:*", + "@clip-wallet/i18n": "workspace:*", + "@clip-wallet/link": "workspace:*", + "@clip-wallet/security": "workspace:*", + "@clip-wallet/social": "workspace:*", + "@clip-wallet/ui": "workspace:*", + "@clip-wallet/vault": "workspace:*", + "@fontsource-variable/inter": "^5.3.0", + "@ledgerhq/hw-transport": "6.36.0", + "@ledgerhq/hw-transport-webhid": "6.36.0", + "@noble/hashes": "2.4.0", + "@vitejs/plugin-react": "^5.1.0", + "buffer": "^6.0.3", + "electron-updater": "^6.8.9", + "esbuild": "^0.25.0", + "zod": "^4.6.5" + }, + "peerDependencies": { + "electron": ">=44.0.0", + "electron-vite": "^5.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "vite": "^7.0.0" + }, + "devDependencies": { + "@types/node": "^22.20.5", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "electron": "44.5.1", + "electron-vite": "^5.0.0", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "typescript": "^5.9.3", + "vite": "^7.1.0", + "vitest": "~4.1.11" + } +} diff --git a/apps/desktop/scripts/build-inpage.mjs b/packages/desktop-kit/scripts/build-inpage.mjs similarity index 100% rename from apps/desktop/scripts/build-inpage.mjs rename to packages/desktop-kit/scripts/build-inpage.mjs diff --git a/packages/desktop-kit/src/builder.ts b/packages/desktop-kit/src/builder.ts new file mode 100644 index 0000000..6cbe7e4 --- /dev/null +++ b/packages/desktop-kit/src/builder.ts @@ -0,0 +1,138 @@ +/** + * The electron-builder config for a Clip Wallet desktop app, from clip.config.ts (https://www.electron.build/configuration): + * + * // electron-builder.config.cjs + * const { electronBuilderConfig } = require("@clip-wallet/desktop-kit/builder"); + * module.exports = electronBuilderConfig({ configFile: "../../clip.config.ts", root: __dirname }); + * + * macOS dmg + zip, arm64 and x64; hardened runtime with build/entitlements.mac*.plist; icon build/icon.icns + * Windows NSIS installer + zip, x64 and arm64; icon build/icon.ico + * Linux AppImage + deb + tar.gz, x64 and arm64; icons build/icons/x.png + * + * App id, product name, executable and artifact names and the deep-link scheme come from platformIds(config). + * Signing and notarization read electron-builder's standard environment variables and are skipped when they are + * absent (local builds and CI without secrets produce unsigned artifacts); see docs/signing.md in a wallet project: + * macOS signing CSC_LINK + CSC_KEY_PASSWORD (Developer ID Application .p12), or CSC_NAME + * macOS notarization APPLE_API_KEY + APPLE_API_KEY_ID + APPLE_API_ISSUER, or APPLE_ID + APPLE_APP_SPECIFIC_PASSWORD + APPLE_TEAM_ID + * Windows signing WIN_CSC_LINK + WIN_CSC_KEY_PASSWORD (or CSC_LINK on a Windows runner) + * Auto-update metadata only for CLIP_UPDATES=1 builds, published to CLIP_UPDATES_OWNER / CLIP_UPDATES_REPO on GitHub. + * Pure apart from reading clip.config.ts when only configFile is given (exported for tests). + */ +import { resolve } from "node:path"; +import { platformIds, type ClipConfig } from "@clip-wallet/config"; +import { loadClipConfigSync } from "@clip-wallet/config/node"; + +export interface BuilderOptions { + /** The resolved clip.config (or give configFile). */ + config?: ClipConfig; + /** clip.config.ts, relative to root (read with loadClipConfigSync). */ + configFile?: string; + /** The desktop project directory. Default: process.cwd(). */ + root?: string; + /** Build environment (signing switches, CLIP_UPDATES). Default: process.env. */ + env?: Record; + /** Icons, relative to root. Default: the ones create-clip-wallet generates from the wallet's logo. */ + icons?: { mac?: string; win?: string; linux?: string }; + /** "Copyright © 2026 Acme". Default: from the name. */ + copyright?: string; + /** Linux package maintainer, "Name ". Default: the wallet's name. */ + maintainer?: string; + /** Linux synopsis. Default: from the description. */ + synopsis?: string; +} + +/** The icon files a desktop project carries (generated from the one logo by create-clip-wallet). */ +export const DESKTOP_ICONS = { mac: "build/icon.icns", win: "build/icon.ico", linux: "build/icons" } as const; + +/** Chromium UI locales to keep for the languages a wallet offers (macOS names some with "_", Windows/Linux with "-"). */ +export function electronLanguages(languages: readonly string[]): string[] { + const out = new Set(); + for (const l of languages) { + if (l === "pt-BR") out.add("pt-BR").add("pt_BR"); + else if (l === "zh-Hans") out.add("zh-CN").add("zh_CN"); + else out.add(l); + } + out.add("en"); + return [...out]; +} + +export function electronBuilderConfig(options: BuilderOptions) { + const root = resolve(options.root ?? process.cwd()); + const config = options.config ?? loadClipConfigSync(options.configFile ?? "clip.config.ts", { cwd: root }); + const env = options.env ?? process.env; + const ids = platformIds(config); + const icons = { ...DESKTOP_ICONS, ...options.icons }; + const macSign = !!(env.CSC_LINK || env.CSC_NAME); + const notarize = + macSign && !!((env.APPLE_API_KEY && env.APPLE_API_KEY_ID && env.APPLE_API_ISSUER) || (env.APPLE_ID && env.APPLE_APP_SPECIFIC_PASSWORD && env.APPLE_TEAM_ID)); + const updates = env.CLIP_UPDATES === "1"; + const testnet = config.mainnet === false; + const synopsis = options.synopsis ?? `${config.description ?? `${config.name}: a non-custodial wallet with a built-in dapp browser.`}${testnet ? " Test networks only." : ""}`; + + return { + appId: ids.desktop.appId, + productName: ids.desktop.productName, + // A name that is safe in file paths (deb package, Linux executable, NSIS archives); the display name stays productName. + extraMetadata: { name: `${ids.desktop.executableName}-desktop` }, + copyright: options.copyright ?? `Copyright © ${new Date().getFullYear()} ${config.name}`, + directories: { output: "release", buildResources: "build" }, + files: ["out/**/*", "package.json", "!out/**/*.map"], + asar: true, + // The native-messaging host program runs with ELECTRON_RUN_AS_NODE from a plain file path. + asarUnpack: ["out/native-host/**"], + npmRebuild: false, + nodeGypRebuild: false, + electronLanguages: electronLanguages(config.languages), + protocols: [{ name: config.name, schemes: [ids.scheme] }], + artifactName: ids.desktop.artifactName, + mac: { + category: "public.app-category.finance", + icon: icons.mac, + target: [ + { target: "dmg", arch: ["arm64", "x64"] }, + { target: "zip", arch: ["arm64", "x64"] }, + ], + hardenedRuntime: true, + gatekeeperAssess: false, + entitlements: "build/entitlements.mac.plist", + entitlementsInherit: "build/entitlements.mac.inherit.plist", + // null = don't sign (and don't pick up a random identity from the keychain) when no certificate is configured. + ...(macSign ? {} : { identity: null }), + notarize, + extendInfo: { + NSCameraUsageDescription: `${config.name} uses the camera only to scan QR codes (WalletConnect codes and Keystone hardware wallets).`, + NSMicrophoneUsageDescription: `${config.name} doesn't use the microphone. Sites you open in the built-in browser have to ask first.`, + }, + }, + dmg: { sign: false }, + win: { + icon: icons.win, + target: [ + { target: "nsis", arch: ["x64", "arm64"] }, + { target: "zip", arch: ["x64", "arm64"] }, + ], + // Editing the .exe resources (icon, version info) and signing use rcedit / signtool: on Windows runners only. + signAndEditExecutable: process.platform === "win32", + }, + nsis: { oneClick: false, perMachine: false, allowToChangeInstallationDirectory: true, deleteAppDataOnUninstall: false }, + linux: { + icon: icons.linux, + category: "Finance", + executableName: ids.desktop.executableName, + synopsis, + maintainer: options.maintainer ?? config.name, + target: [ + { target: "AppImage", arch: ["x64", "arm64"] }, + { target: "deb", arch: ["x64", "arm64"] }, + // Portable archive; also the only Linux format that builds on an Apple-silicon Mac without Rosetta. + { target: "tar.gz", arch: ["x64", "arm64"] }, + ], + }, + // safeStorage uses libsecret (GNOME keyring / KWallet) when present: the deb asks for it. + deb: { depends: ["libgtk-3-0", "libnotify4", "libnss3", "libxss1", "libxtst6", "xdg-utils", "libatspi2.0-0", "libuuid1", "libsecret-1-0"] }, + publish: + updates && env.CLIP_UPDATES_OWNER && env.CLIP_UPDATES_REPO + ? [{ provider: "github", owner: env.CLIP_UPDATES_OWNER, repo: env.CLIP_UPDATES_REPO, releaseType: "release" }] + : null, + }; +} diff --git a/packages/desktop-kit/src/csp.ts b/packages/desktop-kit/src/csp.ts new file mode 100644 index 0000000..3a5e017 --- /dev/null +++ b/packages/desktop-kit/src/csp.ts @@ -0,0 +1,23 @@ +/** + * The wallet and approval windows' Content-Security-Policy: sent as a header by the clip-app: protocol + * (main/app-protocol.ts) and written into the pages' meta tag at build time (electron-vite.ts). No imports, so the + * build config can load it from source. + */ +export function walletCsp(mediaProxyUrl?: string): string { + const media = mediaProxyUrl ? ` ${new URL(mediaProxyUrl).origin}` : ""; + return [ + "default-src 'none'", + "script-src 'self'", + "style-src 'self' 'unsafe-inline'", + `img-src 'self' data: blob:${media}`, + `media-src 'self' blob:${media}`, + "font-src 'self' data:", + "connect-src 'self'", + "worker-src 'none'", + "frame-src 'none'", + "object-src 'none'", + "base-uri 'none'", + "form-action 'none'", + "frame-ancestors 'none'", + ].join("; "); +} diff --git a/packages/desktop-kit/src/electron-vite.ts b/packages/desktop-kit/src/electron-vite.ts new file mode 100644 index 0000000..7959bab --- /dev/null +++ b/packages/desktop-kit/src/electron-vite.ts @@ -0,0 +1,210 @@ +/** + * The electron-vite config for a Clip Wallet desktop app, built from clip.config.ts: + * + * // electron.vite.config.ts + * import { defineConfig } from "electron-vite"; + * import { clipDesktop } from "@clip-wallet/desktop-kit/electron-vite"; + * import clipConfig from "../../clip.config"; + * export default defineConfig(clipDesktop({ config: clipConfig, root: import.meta.dirname, configDir: "../.." })); + * + * Builds the main process (ESM, everything bundled), the sandboxed preloads (CommonJS, one self-contained file each) + * and the three renderer pages, and bundles the native-messaging host program next to them. Identity (name, icon, + * rdns, app id, deep-link scheme) comes from clip.config.ts; a mainnet build is refused until mainnetProblems() is + * empty and MAINNET.md has no open box. There is no option that lowers the security floor. + * + * Build-time switches (public values only; never secrets), from the environment or CLIP_* lines in .env: + * CLIP_WALLETCONNECT_PROJECT_ID WalletConnect (Reown) project id (CLIP_WC_PROJECT_ID also works) + * CLIP_UPDATES=1 turn electron-updater on (signed release builds only) + * CLIP_EXTENSION_IDS Chromium extension ids allowed to use the app over native messaging + * Runs in Node (electron-vite loads it). + */ +import { createRequire } from "node:module"; +import { existsSync, mkdirSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import react from "@vitejs/plugin-react"; +import type { ClipConfig } from "@clip-wallet/config"; +import { buildEnv, iconDataUri, resolveBuildConfig } from "@clip-wallet/config/node"; +import { walletCsp } from "@clip-wallet/desktop-kit/csp"; + +/** The virtual module the kit's main process, preloads and pages import the resolved clip.config from. */ +export const CONFIG_MODULE = "virtual:clip-wallet/config"; + +export interface ClipDesktopOptions { + /** The default export of clip.config.ts (already validated by defineConfig). */ + config: ClipConfig; + /** The desktop project directory (src/, package.json, .env). Default: process.cwd(). */ + root?: string; + /** Where clip.config.ts, its icon, MAINNET.md and the shared .env are, relative to root or absolute. Default: root. */ + configDir?: string; + /** Build environment. Default: process.env plus CLIP_* values from .env in root and configDir. */ + env?: Record; + /** + * Resolve workspace packages from their TypeScript source (the "development" export condition). Default: on when + * Node runs with --conditions=development (the Clip Wallet monorepo sets it); off for installed packages. + */ + sourceConditions?: boolean; +} + +function devConditions(): boolean { + const flags = [...process.execArgv, ...(process.env.NODE_OPTIONS ?? "").split(/\s+/)]; + return flags.some((f, i) => /^(?:--conditions|-C)=development$/.test(f) || ((f === "--conditions" || f === "-C") && flags[i + 1] === "development")); +} + +/** The resolved config and what the build defines from it (exported for tests). */ +export function desktopBuildValues(options: ClipDesktopOptions) { + const root = resolve(options.root ?? process.cwd()); + const configDir = resolve(root, options.configDir ?? "."); + const env = options.env ?? buildEnv([root, configDir]); + const config = resolveBuildConfig(options.config, env, { checklistDir: configDir, fallbackWcEnv: "CLIP_WC_PROJECT_ID" }); + const icon = iconDataUri(configDir, config.icon); + return { + root, + configDir, + config, + icon, + updates: env.CLIP_UPDATES === "1", + extensionIds: env.CLIP_EXTENSION_IDS?.trim() || "", + /** The wallet and approval windows' CSP (also sent as a header by the clip-app: protocol). */ + csp: walletCsp(config.services.mediaProxyUrl), + }; +} + +/** + * Bundle the native-messaging host program (one CommonJS file, Node built-ins only, run by the app's executable with + * ELECTRON_RUN_AS_NODE=1) from the kit's "./native-host" entry. + */ +export async function buildNativeHost(root: string, outFile = join(root, "out", "native-host", "clip-native-host.cjs"), source = devConditions()): Promise { + const { build } = await import("esbuild"); + const req = createRequire(join(root, "package.json")); + let entry: string; + try { + entry = req.resolve("@clip-wallet/desktop-kit/native-host"); + } catch { + // The kit itself (its own tests, or a project that links it): the entry next to this file. + const here = dirname(fileURLToPath(import.meta.url)); + entry = [join(here, "native-host", "main.ts"), join(here, "native-host", "main.js")].find((f) => existsSync(f)) ?? join(here, "native-host", "main.js"); + } + mkdirSync(dirname(outFile), { recursive: true }); + await build({ + entryPoints: [entry], + outfile: outFile, + bundle: true, + ...(source ? { conditions: ["development"] } : {}), + platform: "node", + format: "cjs", + target: "node22", + minify: false, + legalComments: "none", + logLevel: "warning", + }); + return outFile; +} + +const NODE_CONDITIONS = ["development", "module", "node", "development|production"]; +const BROWSER_CONDITIONS = ["development", "module", "browser", "development|production"]; + +/** The electron-vite config (UserConfig). Typed loosely so the kit doesn't pin electron-vite's types. */ +export function clipDesktop(options: ClipDesktopOptions) { + const v = desktopBuildValues(options); + const source = options.sourceConditions ?? devConditions(); + const EMPTY = "\0clip-wallet:empty-module"; + const configModule = `export default ${JSON.stringify(v.config)};\nexport const icon = ${JSON.stringify(v.icon)};\n`; + const wc = JSON.stringify(v.config.walletConnect.projectId ?? ""); + + /** The resolved clip.config as a virtual module; Node's crypto/stream (hdkey via Keystone) as empty modules. */ + const configPlugin = (renderer: boolean) => ({ + name: "clip-wallet:config", + enforce: "pre" as const, + resolveId(id: string) { + if (id === CONFIG_MODULE) return `\0${CONFIG_MODULE}`; + if (renderer && (id === "crypto" || id === "stream")) return EMPTY; + return undefined; + }, + load(id: string) { + if (id === `\0${CONFIG_MODULE}`) return configModule; + if (id === EMPTY) return "export default {};"; + return undefined; + }, + }); + + /** Pages: %CLIP_WALLET_NAME% (the wallet's name) and %CLIP_WALLET_CSP% (the CSP the clip-app: protocol also sends). */ + const htmlPlugin = { + name: "clip-wallet:pages", + transformIndexHtml(html: string) { + const esc = (s: string) => s.replace(/&/g, "&").replace(/ = { ".html": "text/html; charset=utf-8", @@ -15,25 +16,6 @@ export const TYPES: Record = { ".wasm": "application/wasm", }; -export function walletCsp(mediaProxyUrl?: string): string { - const media = mediaProxyUrl ? ` ${new URL(mediaProxyUrl).origin}` : ""; - return [ - "default-src 'none'", - "script-src 'self'", - "style-src 'self' 'unsafe-inline'", - `img-src 'self' data: blob:${media}`, - `media-src 'self' blob:${media}`, - "font-src 'self' data:", - "connect-src 'self'", - "worker-src 'none'", - "frame-src 'none'", - "object-src 'none'", - "base-uri 'none'", - "form-action 'none'", - "frame-ancestors 'none'", - ].join("; "); -} - /** Resolves a clip-app URL path inside `root`, or null (no traversal, no other hosts). */ export function resolveAppPath(root: string, url: string): string | null { let u: URL; diff --git a/apps/desktop/src/main/app-protocol.ts b/packages/desktop-kit/src/main/app-protocol.ts similarity index 100% rename from apps/desktop/src/main/app-protocol.ts rename to packages/desktop-kit/src/main/app-protocol.ts diff --git a/apps/desktop/src/main/biometric.ts b/packages/desktop-kit/src/main/biometric.ts similarity index 100% rename from apps/desktop/src/main/biometric.ts rename to packages/desktop-kit/src/main/biometric.ts diff --git a/packages/desktop-kit/src/main/browser/browser-ua.ts b/packages/desktop-kit/src/main/browser/browser-ua.ts new file mode 100644 index 0000000..22e6e81 --- /dev/null +++ b/packages/desktop-kit/src/main/browser/browser-ua.ts @@ -0,0 +1,11 @@ +/** + * Sites see Chromium's user agent, not ours: drop the Electron token and the app's own ("Acme-Wallet/0.1.0", + * "acme-wallet-desktop/0.1.0"). + */ +export function stripAppTokens(ua: string, name: string): string { + const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-"); + const names = [name, name.replace(/\s+/g, "-"), slug, `${slug}-desktop`, "@clip-wallet/desktop"]; + const re = new RegExp(`\\s(?:Electron|${[...new Set(names)].map(esc).join("|")})/\\S+`, "gi"); + return ua.replace(re, ""); +} diff --git a/apps/desktop/src/main/browser/browser.ts b/packages/desktop-kit/src/main/browser/browser.ts similarity index 98% rename from apps/desktop/src/main/browser/browser.ts rename to packages/desktop-kit/src/main/browser/browser.ts index e68ed67..aec5979 100644 --- a/apps/desktop/src/main/browser/browser.ts +++ b/packages/desktop-kit/src/main/browser/browser.ts @@ -24,6 +24,7 @@ import type { DesktopMessageId } from "../../shared/i18n"; import type { MessageValues } from "@clip-wallet/i18n"; import type { OneMaskRelay } from "./onemask-relay"; import { displayHost, isNavigable, normalizeInput, partitionFor, promptFor, webOrigin } from "./url-policy"; +import { stripAppTokens } from "./browser-ua"; export const CHROME_HEIGHT = 88; const BOOKMARKS = "clip-desktop/bookmarks"; @@ -34,6 +35,8 @@ export interface BrowserDeps { /** clip-app://wallet/browser/index.html */ chromeUrl: string; chromePreload: string; + /** The browser window's title: the wallet's name. */ + title: string; t(id: DesktopMessageId, values?: MessageValues): string; locale(): string; theme(): "light" | "dark"; @@ -100,7 +103,7 @@ export class DappBrowser { private ensureWindow(): BaseWindow { if (this.window) return this.win!; - const win = new BaseWindow({ width: 1200, height: 820, minWidth: 640, minHeight: 480, title: "Clip Wallet", show: false, ...(this.d.iconPath ? { icon: this.d.iconPath } : {}) }); + const win = new BaseWindow({ width: 1200, height: 820, minWidth: 640, minHeight: 480, title: this.d.title, show: false, ...(this.d.iconPath ? { icon: this.d.iconPath } : {}) }); const chrome = new WebContentsView({ webPreferences: { preload: this.d.chromePreload, sandbox: true, contextIsolation: true, nodeIntegration: false, webviewTag: false, spellcheck: false }, }); @@ -396,7 +399,7 @@ export class DappBrowser { private harden(ses: Session) { if (hardened.has(ses)) return; hardened.add(ses); - ses.setUserAgent(ses.getUserAgent().replace(/\s(Electron|clip-wallet|Clip Wallet|@clip-wallet\/desktop)\/\S+/gi, "")); + ses.setUserAgent(stripAppTokens(ses.getUserAgent(), this.d.title)); ses.setSpellCheckerEnabled(false); ses.setPermissionRequestHandler((wc, permission, callback, details) => { const kind = promptFor(permission, (details as { mediaTypes?: string[] }).mediaTypes ?? []); diff --git a/apps/desktop/src/main/browser/onemask-relay.ts b/packages/desktop-kit/src/main/browser/onemask-relay.ts similarity index 100% rename from apps/desktop/src/main/browser/onemask-relay.ts rename to packages/desktop-kit/src/main/browser/onemask-relay.ts diff --git a/apps/desktop/src/main/browser/url-policy.ts b/packages/desktop-kit/src/main/browser/url-policy.ts similarity index 100% rename from apps/desktop/src/main/browser/url-policy.ts rename to packages/desktop-kit/src/main/browser/url-policy.ts diff --git a/apps/desktop/src/main/deeplink.ts b/packages/desktop-kit/src/main/deeplink.ts similarity index 100% rename from apps/desktop/src/main/deeplink.ts rename to packages/desktop-kit/src/main/deeplink.ts diff --git a/apps/desktop/src/main/hid.ts b/packages/desktop-kit/src/main/hid.ts similarity index 100% rename from apps/desktop/src/main/hid.ts rename to packages/desktop-kit/src/main/hid.ts diff --git a/apps/desktop/src/main/host/link.ts b/packages/desktop-kit/src/main/host/link.ts similarity index 100% rename from apps/desktop/src/main/host/link.ts rename to packages/desktop-kit/src/main/host/link.ts diff --git a/apps/desktop/src/main/host/wallet.ts b/packages/desktop-kit/src/main/host/wallet.ts similarity index 100% rename from apps/desktop/src/main/host/wallet.ts rename to packages/desktop-kit/src/main/host/wallet.ts diff --git a/packages/desktop-kit/src/main/index.ts b/packages/desktop-kit/src/main/index.ts new file mode 100644 index 0000000..54b0169 --- /dev/null +++ b/packages/desktop-kit/src/main/index.ts @@ -0,0 +1,673 @@ +/** + * Clip Wallet desktop: main process bootstrap. + * + * wallet window packages/ui WalletApp (same screens, i18n and themes as the extension), clip-app: origin + * approval window packages/ui ApprovalWindowApp, opened by the engine for every request (like the extension's) + * browser window our toolbar + one sandboxed view per tab, one session per site (browser/browser.ts) + * engine + vault here, in the main process (host/wallet.ts); renderers reach it only over checked IPC + * + * Security switches are set before anything loads: single instance, sandbox for every renderer, no remote content + * in app windows (CSP + a request filter on the default session), no new windows, no , no navigation of + * app windows, permission handlers that deny by default. + */ +import { + app, + BrowserWindow, + Menu, + Notification, + dialog, + ipcMain, + nativeTheme, + powerMonitor, + safeStorage, + session, + shell, + systemPreferences, + Tray, + nativeImage, + type IpcMainEvent, + type IpcMainInvokeEvent, + type MenuItemConstructorOptions, + type WebContents, +} from "electron"; +import { mkdtempSync } from "node:fs"; +import { homedir, tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { randomUUID } from "node:crypto"; +import { createTonModule } from "@clip-wallet/chains-ton"; +import type { LocaleCode } from "@clip-wallet/i18n"; +import type { Notice } from "@clip-wallet/social"; +import { CH, ChromeCall, DesktopCall, HidReply, WalletCall, type DesktopInfo, type Envelope, type HidJob, type OneMaskBoot } from "../shared/ipc"; +import { DESKTOP } from "../shared/app-config"; +import { desktopLocale, desktopT } from "../shared/i18n"; +import { APP_ORIGIN, isAppSender, type Role } from "./ipc-guard"; +import { handleAppScheme, registerAppScheme, walletCsp } from "./app-protocol"; +import { createDesktopWallet, type DesktopWallet } from "./host/wallet"; +import { HidRelay, ledgerDevices, LEDGER_VENDOR_ID } from "./hid"; +import { OneMaskRelay, frameOrigin } from "./browser/onemask-relay"; +import { DappBrowser } from "./browser/browser"; +import { isNavigable } from "./browser/url-policy"; +import { deepLinkFromArgv, parseDeepLink } from "./deeplink"; +import { startAutoUpdate, updatesEnabled } from "./updater"; +import { defaultSocketPath } from "@clip-wallet/link/node"; +import { startDesktopLink } from "./host/link"; +import { allowedOrigins, connectorStatus, installConnector, parseExtensionIds, removeConnector, type ConnectorEnv } from "./native-hosts"; + +declare const __CLIP_EXTENSION_IDS__: string; + +const here = fileURLToPath(new URL(".", import.meta.url)); +const PRELOAD_APP = join(here, "../preload/wallet.cjs"); +const PRELOAD_DAPP = join(here, "../preload/dapp.cjs"); +const RENDERER_ROOT = join(here, "../renderer"); +// The native-messaging host program runs from outside app.asar (electron-builder asarUnpack). +const NATIVE_HOST = join(here, "../native-host/clip-native-host.cjs").replace(/app\.asar([\\/])/, "app.asar.unpacked$1"); +/** Dev / tests only (ignored when packaged): where manifests go, the socket, and extra extension ids to trust. */ +const DEV_NM_HOME = !app.isPackaged ? process.env.CLIP_DESKTOP_NM_HOME : undefined; +const DEV_SOCKET = !app.isPackaged ? process.env.CLIP_DESKTOP_SOCKET : undefined; +const DEV_EXT_IDS = !app.isPackaged ? process.env.CLIP_DESKTOP_EXTENSION_IDS : undefined; +/** + * Tests and smoke runs (honoured in packaged builds too, because it only switches things OFF): no browser-connector + * registration, no link socket, no clipwallet:// registration, so a test never touches this machine's browsers. + */ +const NO_SYSTEM_INTEGRATION = process.env.CLIP_DESKTOP_NO_SYSTEM_INTEGRATION === "1"; +const DEV_URL = !app.isPackaged ? process.env.ELECTRON_RENDERER_URL : undefined; +const DEV_ORIGIN = DEV_URL ? new URL(DEV_URL).origin : undefined; + +/* ------------------------------------------------------------------ before ready */ + +// Tests and side-by-side profiles: a separate data folder. Ignored in packaged builds. +if (!app.isPackaged && process.env.CLIP_DESKTOP_USER_DATA) { + app.setPath("userData", process.env.CLIP_DESKTOP_USER_DATA === "temp" ? mkdtempSync(join(tmpdir(), "clip-desktop-")) : process.env.CLIP_DESKTOP_USER_DATA); +} +app.setAppUserModelId(DESKTOP.appId); +registerAppScheme(); +// Never offer Chromium's own password manager / autofill for anything here. +app.commandLine.appendSwitch("disable-features", "AutofillServerCommunication,PasswordManagerOnboarding"); + +if (!app.requestSingleInstanceLock()) { + app.quit(); +} else { + void main(); +} + +function pageUrl(page: "wallet" | "approval" | "browser", hash = ""): string { + return DEV_URL ? `${DEV_URL}/${page}/index.html${hash}` : `${APP_ORIGIN}/${page}/index.html${hash}`; +} + +async function main() { + const pendingLinks: string[] = []; + const first = deepLinkFromArgv(process.argv); + if (first) pendingLinks.push(first); + app.on("open-url", (e, url) => { + e.preventDefault(); + pendingLinks.push(url); + if (app.isReady()) flushLinks(); + }); + app.on("second-instance", (_e, argv) => { + const link = deepLinkFromArgv(argv); + if (link) { + pendingLinks.push(link); + flushLinks(); + } else showWallet(); + }); + + await app.whenReady(); + // macOS registers :// from Info.plist (CFBundleURLTypes); Windows and Linux need it at run time. + if (app.isPackaged && !NO_SYSTEM_INTEGRATION && process.platform !== "darwin" && !app.isDefaultProtocolClient(DESKTOP.scheme)) app.setAsDefaultProtocolClient(DESKTOP.scheme); + handleAppScheme(RENDERER_ROOT, walletCsp(DESKTOP.config.services.mediaProxyUrl)); + + /* ---------------------------------------------------------------- state */ + + const roles = new Map(); + let walletWin: BrowserWindow | null = null; + let approvalWin: BrowserWindow | null = null; + let locale: LocaleCode = desktopLocale(undefined, app.getPreferredSystemLanguages(), DESKTOP.config.languages); + let t = desktopT(locale); + let themePref: "system" | "light" | "dark" = "system"; + const theme = () => (themePref === "system" ? (nativeTheme.shouldUseDarkColors ? "dark" : "light") : themePref); + + const alive = (w: BrowserWindow | null): w is BrowserWindow => !!w && !w.isDestroyed(); + + const hid = new HidRelay({ + target: () => { + const focused = BrowserWindow.getFocusedWindow(); + const pick = [focused, approvalWin, walletWin].find((w) => alive(w) && w.isVisible() && (w === approvalWin || w === walletWin)); + const w = (pick as BrowserWindow | undefined) ?? (alive(walletWin) ? walletWin : null); + return w ? { send: (job: HidJob) => w.webContents.send(CH.hidJob, job) } : null; + }, + }); + + const notifier = { + async show(n: Notice) { + if (!Notification.isSupported()) return; + const note = new Notification({ title: n.title, body: n.body, silent: true }); + note.on("click", () => showWallet(n.route ?? "/")); + note.show(); + }, + }; + + let browser!: DappBrowser; + const wallet: DesktopWallet = createDesktopWallet({ + userData: app.getPath("userData"), + platform: process.platform, + safe: safeStorage, + touchId: { + can: () => process.platform === "darwin" && systemPreferences.canPromptTouchID(), + prompt: (reason) => systemPreferences.promptTouchID(reason), + }, + openApproval: (id) => openApproval(id), + openRoute: (route) => showWallet(route), + broadcast: () => broadcast(), + locked: () => broadcast(), + hidTransport: hid.transport, + notifier, + systemLanguages: () => app.getPreferredSystemLanguages(), + openExternal: async (url) => { + if (new URL(url).protocol === "https:") await shell.openExternal(url); + }, + ...(DESKTOP.wcProjectId ? { wcProjectId: DESKTOP.wcProjectId } : {}), + linkChanged: () => void surfacePairings(), + platformLabel: process.platform === "darwin" ? "macOS" : process.platform === "win32" ? "Windows" : "Linux", + ...(process.env.CLIP_DESKTOP_KNOWN_DAPPS && !app.isPackaged ? { knownDapps: JSON.parse(process.env.CLIP_DESKTOP_KNOWN_DAPPS) as Record } : {}), + }); + + const relay = new OneMaskRelay({ + attach: (port, origin) => wallet.attachDappPort(port, origin), + replyChannel: CH.onemaskToPage, + onActivity: () => browser?.push(), + }); + + /* ---------------------------------------------------------------- linked devices: extension connector */ + + // Extension ids this build trusts: CLIP_EXTENSION_IDS at build time (store ids, plus the unpacked dev id while + // testing); Firefox's add-on id is fixed by the extension's gecko id (wallet@). + const chromiumIds = parseExtensionIds(`${typeof __CLIP_EXTENSION_IDS__ === "string" ? __CLIP_EXTENSION_IDS__ : ""},${DEV_EXT_IDS ?? ""}`); + const firefoxIds = [`wallet@${DESKTOP.config.rdns.split(".").reverse().join(".")}`]; + const socketPath = DEV_SOCKET ?? defaultSocketPath(); + const connector: ConnectorEnv = { + os: process.platform as ConnectorEnv["os"], + rdns: DESKTOP.config.rdns, + home: DEV_NM_HOME ?? homedir(), + ...(process.env.LOCALAPPDATA && !DEV_NM_HOME ? { appData: process.env.LOCALAPPDATA } : DEV_NM_HOME ? { appData: join(DEV_NM_HOME, "AppData", "Local") } : {}), + launcherDir: join(app.getPath("userData"), "native-host"), + execPath: process.execPath, + scriptPath: NATIVE_HOST, + chromiumExtensionIds: chromiumIds, + firefoxAddonIds: firefoxIds, + ...(DEV_SOCKET ? { socketOverride: DEV_SOCKET } : {}), + }; + // Packaged builds (re)register on first run and after every update; dev builds only when asked (Settings). + if (app.isPackaged && !NO_SYSTEM_INTEGRATION && (await wallet.kv.get("clip-desktop/connector-version")) !== app.getVersion()) { + try { + installConnector(connector); + await wallet.kv.set("clip-desktop/connector-version", app.getVersion()); + } catch { + /* Settings → Linked devices shows what's missing and offers "Set up again" */ + } + } + let linkServer: Awaited> | null = null; + try { + if (NO_SYSTEM_INTEGRATION && !DEV_SOCKET) throw new Error("system integration off"); + linkServer = await startDesktopLink(wallet.link, { socketPath, allowedOrigins: allowedOrigins(chromiumIds, firefoxIds) }); + } catch { + linkServer = null; // the socket couldn't be created: the extension says "Clip Desktop isn't running" + } + + /** Pairings already brought to the front (a native pairing from the extension, a scanned code). */ + const surfaced = new Set(); + async function surfacePairings() { + const st = await wallet.link.status().catch(() => null); + for (const p of st?.pairings ?? []) { + if (surfaced.has(p.id) || !["connecting", "compare"].includes(p.state)) continue; + if (p.purpose !== "desktop" && p.state !== "compare") continue; + surfaced.add(p.id); + showWallet(`/settings/devices/pair/${encodeURIComponent(p.id)}`); + } + } + + const channel = `clip-${randomUUID()}`; + const boot: OneMaskBoot = { + channel, + networks: wallet.browserNetworks.map((n) => ({ ...n, rpcUrls: n.rpcUrls.slice(0, 1) })), + identity: DESKTOP.identity, + tonConnect: { key: DESKTOP.walletKey, appName: DESKTOP.walletKey, appVersion: app.getVersion(), features: createTonModule().features }, + }; + + browser = new DappBrowser({ + relay, + preload: PRELOAD_DAPP, + chromePreload: PRELOAD_APP, + chromeUrl: pageUrl("browser"), + title: DESKTOP.config.name, + t: (id, v) => t(id, v), + locale: () => locale, + theme, + kv: wallet.kv, + isKnownScam: (o) => wallet.isKnownScam(o), + scamReasons: async (o) => (await wallet.checkSite(o)).reasons, + connectedOrigins: () => wallet.connectedOrigins(), + featured: () => wallet.featured(), + registerChrome: (wc) => register(wc, "chrome"), + }); + + /* ---------------------------------------------------------------- app windows */ + + function register(wc: WebContents, role: Role) { + roles.set(wc.id, role); + const id = wc.id; + wc.once("destroyed", () => roles.delete(id)); + } + + const appWebPreferences = { + preload: PRELOAD_APP, + sandbox: true, + contextIsolation: true, + nodeIntegration: false, + webviewTag: false, + spellcheck: false, + navigateOnDragDrop: false, + } as const; + + function showWallet(route?: string) { + if (!alive(walletWin)) { + walletWin = new BrowserWindow({ + width: 440, + height: 780, + minWidth: 360, + minHeight: 560, + title: DESKTOP.config.name, + show: false, + backgroundColor: theme() === "dark" ? "#0E0F12" : "#FFFFFF", + webPreferences: appWebPreferences, + }); + register(walletWin.webContents, "wallet"); + void walletWin.loadURL(pageUrl("wallet", route ? `#${route}` : "")); + walletWin.once("ready-to-show", () => walletWin?.show()); + walletWin.on("closed", () => (walletWin = null)); + return; + } + if (route) void walletWin.webContents.executeJavaScript(`location.hash = ${JSON.stringify(route)}`).catch(() => undefined); + if (walletWin.isMinimized()) walletWin.restore(); + walletWin.show(); + walletWin.focus(); + } + + function openApproval(id: string) { + // A request may come from the browser extension (Clip Desktop as its signer) while another app is in front: + // the approval must be where the person is looking. + if (process.platform === "darwin") app.focus({ steal: true }); + if (alive(approvalWin)) { + approvalWin.show(); + approvalWin.focus(); + return; + } + approvalWin = new BrowserWindow({ + width: 400, + height: 680, + minWidth: 360, + minHeight: 520, + title: DESKTOP.config.name, + show: false, + alwaysOnTop: true, + fullscreenable: false, + backgroundColor: theme() === "dark" ? "#0E0F12" : "#FFFFFF", + webPreferences: appWebPreferences, + }); + register(approvalWin.webContents, "approval"); + void approvalWin.loadURL(pageUrl("approval", `#${encodeURIComponent(id)}`)); + approvalWin.once("ready-to-show", () => { + approvalWin?.show(); + approvalWin?.focus(); + }); + approvalWin.on("closed", () => (approvalWin = null)); + } + + async function broadcast() { + for (const w of [walletWin, approvalWin]) if (alive(w)) w.webContents.send(CH.walletChanged); + browser.push(); + if (pendingHandoff && (await wallet.engine.deps.vault.status()) === "unlocked") void openHandoff(pendingHandoff.link, pendingHandoff.url); + // Language and theme follow Settings. + const prefs = await wallet.engine.prefs().catch(() => null); + if (prefs) { + themePref = prefs.theme; + const next = desktopLocale(prefs.locale, app.getPreferredSystemLanguages(), DESKTOP.config.languages); + if (next !== locale) { + locale = next; + t = desktopT(locale); + buildMenu(); + buildTray(); + } + } + } + + /* ---------------------------------------------------------------- sessions */ + + const ses = session.defaultSession; + // App windows load nothing from the network: only the bundle (clip-app:), data:/blob:, devtools, and NFT images + // through the configured media proxy. + const mediaOrigin = DESKTOP.config.services.mediaProxyUrl ? new URL(DESKTOP.config.services.mediaProxyUrl).origin : null; + ses.webRequest.onBeforeRequest({ urls: ["http://*/*", "https://*/*", "ws://*/*", "wss://*/*", "file://*/*", "ftp://*/*"] }, (d, cb) => { + const origin = (() => { + try { + return new URL(d.url).origin; + } catch { + return ""; + } + })(); + const ok = (DEV_ORIGIN && origin === DEV_ORIGIN) || (mediaOrigin && origin === mediaOrigin && (d.resourceType === "image" || d.resourceType === "media")); + cb({ cancel: !ok }); + }); + ses.setPermissionRequestHandler((wc, permission, callback, details) => { + const isApp = roles.has(wc.id) && details.isMainFrame && (details.requestingUrl.startsWith(`${APP_ORIGIN}/`) || (!!DEV_ORIGIN && details.requestingUrl.startsWith(DEV_ORIGIN))); + const media = (details as { mediaTypes?: string[] }).mediaTypes ?? []; + // The wallet's own QR scanner (WalletConnect, Keystone): camera only, never the microphone. + if (isApp && permission === "media" && media.length > 0 && media.every((m) => m === "video")) return callback(true); + if (isApp && permission === "clipboard-sanitized-write") return callback(true); + callback(false); + }); + ses.setPermissionCheckHandler((wc, permission, requestingOrigin) => { + if (!wc || !roles.has(wc.id)) return false; + const isApp = requestingOrigin === APP_ORIGIN || requestingOrigin === `${APP_ORIGIN}/` || (!!DEV_ORIGIN && requestingOrigin.startsWith(DEV_ORIGIN)); + return isApp && (permission === "hid" || permission === "media" || permission === "clipboard-sanitized-write"); + }); + // WebHID: Ledger only, for our own pages; the picker is a native dialog with an explicit choice. + const pickedHid = new Set(); + ses.setDevicePermissionHandler((d) => { + const isApp = d.origin === APP_ORIGIN || d.origin === `${APP_ORIGIN}/` || (!!DEV_ORIGIN && d.origin.startsWith(DEV_ORIGIN)); + const dev = d.device as { vendorId?: number; deviceId?: string }; + // Only a Ledger the user picked in this run's chooser (so the approval window can reopen it without asking). + return isApp && d.deviceType === "hid" && dev.vendorId === LEDGER_VENDOR_ID && !!dev.deviceId && pickedHid.has(dev.deviceId); + }); + ses.on("select-hid-device", (event, details, callback) => { + event.preventDefault(); + // Only our windows use the default session (dapp tabs have their own sessions, which deny HID outright). + const devices = ledgerDevices(details.deviceList); + const parent = BrowserWindow.getFocusedWindow() ?? walletWin ?? undefined; + if (!devices.length) { + const opts = { type: "info" as const, message: t("d.hid.title"), detail: t("d.hid.none"), buttons: ["OK"] }; + void (parent ? dialog.showMessageBox(parent, opts) : dialog.showMessageBox(opts)); + return callback(""); + } + const opts = { + type: "question" as const, + message: t("d.hid.title"), + detail: t("d.hid.body", { name: DESKTOP.config.name }), + buttons: [...devices.map((d) => d.name || "Ledger"), t("d.common.cancel")], + cancelId: devices.length, + defaultId: devices.length, + }; + void (parent ? dialog.showMessageBox(parent, opts) : dialog.showMessageBox(opts)).then((r) => { + const d = devices[r.response]; + if (!d) return callback(""); + pickedHid.add(d.deviceId); + callback(d.deviceId); + }); + }); + ses.on("select-serial-port", (e, _l, _wc, cb) => (e.preventDefault(), cb(""))); + ses.on("select-usb-device", (e, _d, cb) => (e.preventDefault(), cb())); + ses.on("will-download", (e) => e.preventDefault()); + + app.on("web-contents-created", (_e, wc) => { + wc.on("will-attach-webview", (e) => e.preventDefault()); + // App windows never navigate and never open windows. (Dapp tabs replace both handlers in browser.ts.) + wc.setWindowOpenHandler(({ url }) => { + if (roles.has(wc.id) && /^https:\/\//.test(url)) void shell.openExternal(url); + return { action: "deny" }; + }); + wc.on("will-navigate", (e) => { + if (!browser.isTabContents(wc.id)) e.preventDefault(); + }); + }); + + /* ---------------------------------------------------------------- IPC */ + + const toEnvelope = (e: unknown): Envelope => { + const u = e as { userMessage?: unknown; code?: unknown; msg?: unknown } | null; + if (u && typeof u.userMessage === "string" && typeof u.code === "string") + return { ok: false, error: { userMessage: u.userMessage, code: u.code, ...(u.msg !== undefined ? { msg: u.msg } : {}) } }; + return { ok: false, error: { userMessage: "Something went wrong. Please try again.", code: "internal" } }; + }; + const denied: Envelope = { ok: false, error: { userMessage: "Something went wrong. Please try again.", code: "bus/forbidden" } }; + const invalid: Envelope = { ok: false, error: { userMessage: "Something went wrong. Please try again.", code: "bus/invalid" } }; + const guard = (e: IpcMainEvent | IpcMainInvokeEvent, allowed: Role[]) => isAppSender(e as never, roles, allowed, DEV_ORIGIN); + + ipcMain.handle(CH.walletCall, async (e, raw: unknown): Promise => { + if (!guard(e, ["wallet", "approval"])) return denied; + const msg = WalletCall.safeParse(raw); + if (!msg.success) return invalid; + try { + return { ok: true, data: await wallet.handle(msg.data as never) }; + } catch (err) { + return toEnvelope(err); + } + }); + + ipcMain.handle(CH.desktopCall, async (e, raw: unknown): Promise => { + if (!guard(e, ["wallet", "approval"])) return denied; + const m = DesktopCall.safeParse(raw); + if (!m.success) return invalid; + try { + const c = m.data; + switch (c.op) { + case "info": { + const info: DesktopInfo = { + platform: process.platform, + appVersion: app.getVersion(), + electron: process.versions.electron, + chrome: process.versions.chrome, + storage: wallet.protection, + biometrics: wallet.biometrics, + updates: updatesEnabled() ? "enabled" : "disabled", + }; + return { ok: true, data: info }; + } + case "openBrowser": + if (c.url && !isNavigable(c.url)) return invalid; + browser.newTab(c.url); + return { ok: true }; + case "openExternal": + if (!c.url.startsWith("https://")) return invalid; + await shell.openExternal(c.url); + return { ok: true }; + case "showWallet": + showWallet(c.route); + return { ok: true }; + case "bioEnroll": + return { ok: true, data: await wallet.bioEnroll(c.ceremonyId, c.prfInput, t("d.bio.enroll", { name: DESKTOP.config.name })) }; + case "bioEvaluate": + await wallet.bioEvaluate(c.ceremonyId, c.credentialId, c.prfInput, t("d.bio.unlock", { name: DESKTOP.config.name })); + return { ok: true }; + case "connectorStatus": + return { ok: true, data: connectorStatus(connector) }; + case "connectorRepair": { + if (NO_SYSTEM_INTEGRATION && !DEV_NM_HOME) return invalid; + const v = installConnector(connector); + await wallet.kv.set("clip-desktop/connector-version", app.getVersion()); + return { ok: true, data: v }; + } + case "connectorRemove": + return { ok: true, data: removeConnector(connector) }; + case "closeSelf": + BrowserWindow.fromWebContents(e.sender)?.close(); + return { ok: true }; + } + } catch (err) { + return toEnvelope(err); + } + }); + + ipcMain.on(CH.hidReply, (e, raw: unknown) => { + if (!guard(e, ["wallet", "approval"])) return; + const r = HidReply.safeParse(raw); + if (r.success) hid.onReply(r.data); + }); + + ipcMain.handle(CH.chromeCall, async (e, raw: unknown): Promise => { + if (!guard(e, ["chrome"])) return denied; + const m = ChromeCall.safeParse(raw); + if (!m.success) return invalid; + const c = m.data; + switch (c.op) { + case "state": + return { ok: true, data: await browser.state() }; + case "newTab": + if (c.url !== undefined && !isNavigable(c.url)) return invalid; + browser.newTab(c.url); + return { ok: true }; + case "closeTab": + browser.closeTab(c.tabId); + return { ok: true }; + case "selectTab": + browser.selectTab(c.tabId); + return { ok: true }; + case "navigate": + return browser.navigate(c.tabId, c.input) ? { ok: true } : { ok: false, error: { userMessage: t("d.browser.badUrl"), code: "browser/bad-url" } }; + case "back": + browser.back(c.tabId); + return { ok: true }; + case "forward": + browser.forward(c.tabId); + return { ok: true }; + case "reload": + browser.reload(c.tabId); + return { ok: true }; + case "stop": + browser.stop(c.tabId); + return { ok: true }; + case "toggleBookmark": + await browser.toggleBookmark(c.tabId); + return { ok: true }; + case "removeBookmark": + await browser.removeBookmark(c.url); + return { ok: true }; + case "answer": + browser.answer(c.promptId, c.choice); + return { ok: true }; + case "openWallet": + showWallet(c.route); + return { ok: true }; + } + }); + + // 1Mask: boot config and requests, only from the top frame of a dapp tab. + ipcMain.on(CH.onemaskBoot, (e) => { + e.returnValue = browser.isTabContents(e.sender.id) && frameOrigin(e.sender as never, e.senderFrame as never) ? boot : null; + }); + ipcMain.on(CH.onemaskToMain, (e, raw: unknown) => { + if (!browser.isTabContents(e.sender.id)) return; + relay.onMessage(e.sender as never, e.senderFrame as never, raw); + }); + + /* ---------------------------------------------------------------- lifecycle */ + + powerMonitor.on("lock-screen", () => void wallet.lock()); + powerMonitor.on("suspend", () => void wallet.lock()); + nativeTheme.on("updated", () => browser.push()); + + function buildMenu() { + const name = DESKTOP.config.name; + const template: MenuItemConstructorOptions[] = [ + ...(process.platform === "darwin" + ? [{ label: name, submenu: [{ role: "about" }, { type: "separator" }, { role: "hide" }, { role: "hideOthers" }, { type: "separator" }, { label: t("d.menu.quit", { name }), role: "quit" }] } as MenuItemConstructorOptions] + : []), + { + label: t("d.menu.wallet"), + submenu: [ + { label: t("d.menu.wallet"), accelerator: "CmdOrCtrl+1", click: () => showWallet() }, + { label: t("d.menu.lock"), accelerator: "CmdOrCtrl+Shift+L", click: () => void wallet.lock() }, + ...(process.platform === "darwin" ? [] : [{ type: "separator" } as MenuItemConstructorOptions, { label: t("d.menu.quit", { name }), role: "quit" } as MenuItemConstructorOptions]), + ], + }, + { + label: t("d.menu.browser"), + submenu: [ + { label: t("d.menu.browser"), accelerator: "CmdOrCtrl+2", click: () => (browser.window ? browser.focus() : browser.newTab()) }, + { label: t("d.menu.newTab"), accelerator: "CmdOrCtrl+T", click: () => browser.newTab() }, + ], + }, + { label: t("d.menu.edit"), role: "editMenu" }, + { label: t("d.menu.view"), submenu: [{ role: "resetZoom" }, { role: "zoomIn" }, { role: "zoomOut" }, { type: "separator" }, { role: "togglefullscreen" }, ...(app.isPackaged ? [] : [{ role: "toggleDevTools" } as MenuItemConstructorOptions])] }, + { label: t("d.menu.window"), role: "windowMenu" }, + ]; + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); + } + + /** + * "Continue on this device": the token can only be checked with the unlocked wallet, so a locked wallet asks to + * unlock first and the handoff waits. Linked devices then shows "Continue on "; one tap restores the + * connection for that site and opens it in the built-in browser. + */ + let pendingHandoff: { link: string; url: string } | null = null; + async function openHandoff(raw: string, url: string) { + if ((await wallet.engine.deps.vault.status()) !== "unlocked") { + pendingHandoff = { link: raw, url }; + showWallet(); + return; + } + pendingHandoff = null; + try { + await wallet.link.handoffOpen(raw); + showWallet("/settings/devices"); + } catch { + browser.newTab(url); + } + } + + /* tray / menu bar: open the wallet or the browser, lock, quit */ + let tray: Tray | null = null; + function buildTray() { + const name = DESKTOP.config.name; + if (!tray) { + const icon = nativeImage.createFromPath(join(RENDERER_ROOT, "tray", process.platform === "darwin" ? "trayTemplate.png" : "tray.png")); + if (process.platform === "darwin") icon.setTemplateImage(true); + tray = new Tray(icon); + tray.setToolTip(name); + tray.on("click", () => showWallet()); + } + tray.setContextMenu( + Menu.buildFromTemplate([ + { label: t("d.tray.open", { name }), click: () => showWallet() }, + { label: t("d.menu.browser"), click: () => (browser.window ? browser.focus() : browser.newTab()) }, + { type: "separator" }, + { label: t("d.menu.lock"), click: () => void wallet.lock() }, + { label: t("d.menu.quit", { name }), role: "quit" }, + ]), + ); + } + + function flushLinks() { + while (pendingLinks.length) { + const link = parseDeepLink(pendingLinks.shift()); + if (!link) continue; + if (link.kind === "browse") browser.newTab(link.url); + else if (link.kind === "trade") showWallet(link.route); + else if (link.kind === "handoff") void openHandoff(link.link, link.url); + else if (link.kind === "pair") + void wallet.link.scan(link.uri).then( + (p) => (surfaced.add(p.id), showWallet(`/settings/devices/pair/${encodeURIComponent(p.id)}`)), + () => showWallet("/settings/devices"), + ); + else { + showWallet(); + void wallet.pairWalletConnect(link.uri).catch(() => showWallet("/scan")); + } + } + } + + app.on("activate", () => showWallet()); + app.on("window-all-closed", () => { + if (process.platform !== "darwin") app.quit(); + }); + app.on("before-quit", () => { + wallet.dispose(); + void linkServer?.stop(); + }); + + await broadcast().catch(() => undefined); + buildMenu(); + buildTray(); + showWallet(); + flushLinks(); + void startAutoUpdate(); +} diff --git a/apps/desktop/src/main/ipc-guard.ts b/packages/desktop-kit/src/main/ipc-guard.ts similarity index 100% rename from apps/desktop/src/main/ipc-guard.ts rename to packages/desktop-kit/src/main/ipc-guard.ts diff --git a/apps/desktop/src/main/native-hosts.ts b/packages/desktop-kit/src/main/native-hosts.ts similarity index 100% rename from apps/desktop/src/main/native-hosts.ts rename to packages/desktop-kit/src/main/native-hosts.ts diff --git a/apps/desktop/src/main/storage.ts b/packages/desktop-kit/src/main/storage.ts similarity index 100% rename from apps/desktop/src/main/storage.ts rename to packages/desktop-kit/src/main/storage.ts diff --git a/apps/desktop/src/main/updater.ts b/packages/desktop-kit/src/main/updater.ts similarity index 100% rename from apps/desktop/src/main/updater.ts rename to packages/desktop-kit/src/main/updater.ts diff --git a/apps/desktop/src/native-host/main.ts b/packages/desktop-kit/src/native-host/main.ts similarity index 100% rename from apps/desktop/src/native-host/main.ts rename to packages/desktop-kit/src/native-host/main.ts diff --git a/packages/desktop-kit/src/preload/dapp.ts b/packages/desktop-kit/src/preload/dapp.ts new file mode 100644 index 0000000..6b17e50 --- /dev/null +++ b/packages/desktop-kit/src/preload/dapp.ts @@ -0,0 +1,45 @@ +/** + * Preload of every dapp tab (sandboxed, isolated world; the page cannot see this script or anything it holds). + * + * 1. Asks the main process for the 1Mask boot config. The main process answers only for the top frame of a + * dapp tab on an http(s) origin; preloads don't run in iframes at all (no nodeIntegrationInSubFrames). + * 2. Runs the 1Mask inpage providers in the page's MAIN world, synchronously, before any page script. + * 3. Runs 1Mask's content bridge here, in the ISOLATED world: it accepts only same-window postMessages on the + * run's channel that pass the strict schema, and relays them to the main process over IPC. The main process + * replaces the origin with the frame's committed origin (onemask-relay.ts). + * + * Nothing is exposed to the page: no contextBridge.exposeInMainWorld, so window.require, ipcRenderer, process and + * electron stay out of its reach. + */ +import { contextBridge, ipcRenderer } from "electron"; +import { createContentBridge, type RuntimePort } from "@clip-wallet/1mask/content"; +import { CH, type OneMaskBoot } from "../shared/ipc"; +import { inpageMain } from "./inpage.generated"; + +function start() { + if (location.protocol !== "https:" && location.protocol !== "http:") return; + let boot: OneMaskBoot | null = null; + try { + boot = ipcRenderer.sendSync(CH.onemaskBoot) as OneMaskBoot | null; + } catch { + boot = null; + } + if (!boot || typeof boot.channel !== "string") return; + + contextBridge.executeInMainWorld({ func: inpageMain, args: [boot] }); + + const messageListeners = new Set<(m: unknown) => void>(); + const disconnectListeners = new Set<() => void>(); + ipcRenderer.on(CH.onemaskToPage, (_e, msg: unknown) => { + for (const l of messageListeners) l(msg); + }); + const port: RuntimePort = { + postMessage: (message) => ipcRenderer.send(CH.onemaskToMain, message), + onMessage: { addListener: (cb) => void messageListeners.add(cb) }, + onDisconnect: { addListener: (cb) => void disconnectListeners.add(cb) }, + disconnect: () => undefined, + }; + createContentBridge({ channel: boot.channel, connect: () => port }); +} + +start(); diff --git a/apps/desktop/src/preload/inpage-entry.ts b/packages/desktop-kit/src/preload/inpage-entry.ts similarity index 100% rename from apps/desktop/src/preload/inpage-entry.ts rename to packages/desktop-kit/src/preload/inpage-entry.ts diff --git a/packages/desktop-kit/src/preload/wallet.ts b/packages/desktop-kit/src/preload/wallet.ts new file mode 100644 index 0000000..a927899 --- /dev/null +++ b/packages/desktop-kit/src/preload/wallet.ts @@ -0,0 +1,40 @@ +/** + * Preload of the wallet, approval and browser-toolbar windows (sandboxed, isolated world). It exposes one small, + * typed bridge object; the renderer never gets ipcRenderer itself. Every call is checked again in the main + * process (schema, then sender: ipc-guard.ts). + */ +import { contextBridge, ipcRenderer } from "electron"; +import { CH, type ChromeCall, type ChromeState, type DesktopCall, type HidJob, type HidReply } from "../shared/ipc"; + +const api = { + platform: process.platform, + /** Wallet / features / social / security / hardware message → Envelope. */ + call: (msg: unknown): Promise => ipcRenderer.invoke(CH.walletCall, msg), + /** Desktop-only calls → Envelope. */ + desktop: (msg: DesktopCall): Promise => ipcRenderer.invoke(CH.desktopCall, msg), + onChange(cb: () => void): () => void { + const h = () => cb(); + ipcRenderer.on(CH.walletChanged, h); + return () => void ipcRenderer.removeListener(CH.walletChanged, h); + }, + /** Ledger APDU jobs (the WebHID half of hid.ts). */ + onHidJob(handler: (job: HidJob) => Promise): void { + ipcRenderer.on(CH.hidJob, (_e, job: HidJob) => { + void handler(job).then( + (r) => ipcRenderer.send(CH.hidReply, r), + (e: unknown) => ipcRenderer.send(CH.hidReply, { id: job.id, ok: false, name: "Error", message: String((e as Error)?.message ?? e).slice(0, 500) }), + ); + }); + }, + /** Browser toolbar only. */ + chrome: (msg: ChromeCall): Promise => ipcRenderer.invoke(CH.chromeCall, msg), + onChromeState(cb: (s: ChromeState) => void): () => void { + const h = (_e: unknown, s: ChromeState) => cb(s); + ipcRenderer.on(CH.chromeState, h); + return () => void ipcRenderer.removeListener(CH.chromeState, h); + }, +}; + +export type ClipDesktopApi = typeof api; + +contextBridge.exposeInMainWorld("clipDesktop", api); diff --git a/apps/desktop/src/renderer/browser/BrowserChrome.tsx b/packages/desktop-kit/src/renderer/browser/BrowserChrome.tsx similarity index 100% rename from apps/desktop/src/renderer/browser/BrowserChrome.tsx rename to packages/desktop-kit/src/renderer/browser/BrowserChrome.tsx diff --git a/apps/desktop/src/renderer/browser/browser.css b/packages/desktop-kit/src/renderer/browser/browser.css similarity index 100% rename from apps/desktop/src/renderer/browser/browser.css rename to packages/desktop-kit/src/renderer/browser/browser.css diff --git a/packages/desktop-kit/src/renderer/browser/mount.tsx b/packages/desktop-kit/src/renderer/browser/mount.tsx new file mode 100644 index 0000000..571fcdc --- /dev/null +++ b/packages/desktop-kit/src/renderer/browser/mount.tsx @@ -0,0 +1,15 @@ +import "../shared/globals"; +import "@fontsource-variable/inter"; +import "@clip-wallet/desktop-kit/browser.css"; +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserChrome } from "./BrowserChrome"; + +/** The built-in browser's toolbar window (src/renderer/browser/main.tsx in a wallet project). */ +export function mountBrowserChrome(): void { + createRoot(document.getElementById("root")!).render( + + + , + ); +} diff --git a/packages/desktop-kit/src/renderer/index.ts b/packages/desktop-kit/src/renderer/index.ts new file mode 100644 index 0000000..5685c56 --- /dev/null +++ b/packages/desktop-kit/src/renderer/index.ts @@ -0,0 +1,8 @@ +/** + * The desktop app's three renderer pages, one call each (a wallet project's src/renderer//main.tsx): + * wallet mountWallet() the wallet window (the extension's screens from @clip-wallet/ui) + * approval mountApproval() the approval window + * browser mountBrowserChrome() the built-in browser's toolbar + */ +export { mountApproval, mountWallet } from "./shared/mount"; +export { mountBrowserChrome } from "./browser/mount"; diff --git a/apps/desktop/src/renderer/shared/bridge.ts b/packages/desktop-kit/src/renderer/shared/bridge.ts similarity index 100% rename from apps/desktop/src/renderer/shared/bridge.ts rename to packages/desktop-kit/src/renderer/shared/bridge.ts diff --git a/apps/desktop/src/renderer/shared/clients.ts b/packages/desktop-kit/src/renderer/shared/clients.ts similarity index 100% rename from apps/desktop/src/renderer/shared/clients.ts rename to packages/desktop-kit/src/renderer/shared/clients.ts diff --git a/apps/desktop/src/renderer/shared/empty-module.ts b/packages/desktop-kit/src/renderer/shared/empty-module.ts similarity index 100% rename from apps/desktop/src/renderer/shared/empty-module.ts rename to packages/desktop-kit/src/renderer/shared/empty-module.ts diff --git a/apps/desktop/src/renderer/shared/globals.ts b/packages/desktop-kit/src/renderer/shared/globals.ts similarity index 100% rename from apps/desktop/src/renderer/shared/globals.ts rename to packages/desktop-kit/src/renderer/shared/globals.ts diff --git a/apps/desktop/src/renderer/shared/hid-agent.ts b/packages/desktop-kit/src/renderer/shared/hid-agent.ts similarity index 100% rename from apps/desktop/src/renderer/shared/hid-agent.ts rename to packages/desktop-kit/src/renderer/shared/hid-agent.ts diff --git a/apps/desktop/src/renderer/shared/mount.tsx b/packages/desktop-kit/src/renderer/shared/mount.tsx similarity index 94% rename from apps/desktop/src/renderer/shared/mount.tsx rename to packages/desktop-kit/src/renderer/shared/mount.tsx index 774f79d..8c7f807 100644 --- a/apps/desktop/src/renderer/shared/mount.tsx +++ b/packages/desktop-kit/src/renderer/shared/mount.tsx @@ -11,7 +11,8 @@ import { desktop } from "./bridge"; import { startHidAgent } from "./hid-agent"; const options: Partial = { - iconUrl: new URL("../icon.svg", location.href).toString(), + // The wallet's own icon (clip.config icon, inlined at build time): no file to keep in step. + iconUrl: DESKTOP.identity.icon, ...(DESKTOP.config.services.mediaProxyUrl ? { mediaProxyUrl: DESKTOP.config.services.mediaProxyUrl } : {}), currencies: ["USD", "EUR", "GBP", "JPY", "CHF", "CAD", "AUD"], }; diff --git a/packages/desktop-kit/src/shared/app-config.ts b/packages/desktop-kit/src/shared/app-config.ts new file mode 100644 index 0000000..e1f2939 --- /dev/null +++ b/packages/desktop-kit/src/shared/app-config.ts @@ -0,0 +1,33 @@ +/** + * Build-time settings shared by the main process and the renderers. Public identifiers only, never secrets. + * The wallet's validated clip.config comes from `virtual:clip-wallet/config`, which clipDesktop() + * ("@clip-wallet/desktop-kit/electron-vite") resolves to the project's clip.config.ts, with the WalletConnect project + * id from the build environment applied, plus the icon as a data URI (tests alias the module). + */ +import { platformIds, walletKey, type ClipConfig } from "@clip-wallet/config"; +// @ts-ignore: a virtual module, resolved by the host bundler (electron-vite.ts) or the test runner. +import raw, { icon as rawIcon } from "virtual:clip-wallet/config"; + +const clipConfig: ClipConfig = raw; +const icon: string = rawIcon; +const ids = platformIds(clipConfig); +const wcProjectId = clipConfig.walletConnect.projectId || undefined; +const site = clipConfig.homepage ?? `https://${walletKey(clipConfig)}.example`; + +export const DESKTOP = { + config: clipConfig, + wcProjectId, + /** WalletConnect metadata: the wallet's homepage, or a placeholder until it has one. */ + siteUrl: site, + iconUrl: `${site}/icon.png`, + /** EIP-6963 / Wallet Standard identity (icon must be a data URI). */ + identity: { name: clipConfig.name, rdns: clipConfig.rdns, icon }, + /** Deep links: ://wc?uri=…, ://browse?url=…, ://trade#offer=… */ + scheme: ids.scheme, + /** Secure Trade share links open the app. */ + tradeLinkBase: `${ids.scheme}://trade`, + /** Bundle / app id (electron-builder appId, Windows AppUserModelID). */ + appId: ids.desktop.appId, + /** TON Connect JS bridge key and app name (must match the wallets-list entry). */ + walletKey: walletKey(clipConfig), +} as const; diff --git a/apps/desktop/src/shared/i18n/ar.ts b/packages/desktop-kit/src/shared/i18n/ar.ts similarity index 100% rename from apps/desktop/src/shared/i18n/ar.ts rename to packages/desktop-kit/src/shared/i18n/ar.ts diff --git a/apps/desktop/src/shared/i18n/de.ts b/packages/desktop-kit/src/shared/i18n/de.ts similarity index 100% rename from apps/desktop/src/shared/i18n/de.ts rename to packages/desktop-kit/src/shared/i18n/de.ts diff --git a/apps/desktop/src/shared/i18n/en.ts b/packages/desktop-kit/src/shared/i18n/en.ts similarity index 100% rename from apps/desktop/src/shared/i18n/en.ts rename to packages/desktop-kit/src/shared/i18n/en.ts diff --git a/apps/desktop/src/shared/i18n/es.ts b/packages/desktop-kit/src/shared/i18n/es.ts similarity index 100% rename from apps/desktop/src/shared/i18n/es.ts rename to packages/desktop-kit/src/shared/i18n/es.ts diff --git a/apps/desktop/src/shared/i18n/fr.ts b/packages/desktop-kit/src/shared/i18n/fr.ts similarity index 100% rename from apps/desktop/src/shared/i18n/fr.ts rename to packages/desktop-kit/src/shared/i18n/fr.ts diff --git a/apps/desktop/src/shared/i18n/hi.ts b/packages/desktop-kit/src/shared/i18n/hi.ts similarity index 100% rename from apps/desktop/src/shared/i18n/hi.ts rename to packages/desktop-kit/src/shared/i18n/hi.ts diff --git a/apps/desktop/src/shared/i18n/index.ts b/packages/desktop-kit/src/shared/i18n/index.ts similarity index 90% rename from apps/desktop/src/shared/i18n/index.ts rename to packages/desktop-kit/src/shared/i18n/index.ts index 8b25300..27c3c15 100644 --- a/apps/desktop/src/shared/i18n/index.ts +++ b/packages/desktop-kit/src/shared/i18n/index.ts @@ -40,6 +40,6 @@ export function desktopT(locale: LocaleCode): TFunction { } /** The locale for a Settings → Language preference and the OS's languages. */ -export function desktopLocale(pref: LocalePref | undefined, systemLanguages: readonly string[]): LocaleCode { - return resolveLocale(pref, systemLanguages); +export function desktopLocale(pref: LocalePref | undefined, systemLanguages: readonly string[], offered?: readonly LocaleCode[]): LocaleCode { + return resolveLocale(pref, systemLanguages, offered); } diff --git a/apps/desktop/src/shared/i18n/it.ts b/packages/desktop-kit/src/shared/i18n/it.ts similarity index 100% rename from apps/desktop/src/shared/i18n/it.ts rename to packages/desktop-kit/src/shared/i18n/it.ts diff --git a/apps/desktop/src/shared/i18n/ja.ts b/packages/desktop-kit/src/shared/i18n/ja.ts similarity index 100% rename from apps/desktop/src/shared/i18n/ja.ts rename to packages/desktop-kit/src/shared/i18n/ja.ts diff --git a/apps/desktop/src/shared/i18n/ko.ts b/packages/desktop-kit/src/shared/i18n/ko.ts similarity index 100% rename from apps/desktop/src/shared/i18n/ko.ts rename to packages/desktop-kit/src/shared/i18n/ko.ts diff --git a/apps/desktop/src/shared/i18n/pt-BR.ts b/packages/desktop-kit/src/shared/i18n/pt-BR.ts similarity index 100% rename from apps/desktop/src/shared/i18n/pt-BR.ts rename to packages/desktop-kit/src/shared/i18n/pt-BR.ts diff --git a/apps/desktop/src/shared/i18n/tr.ts b/packages/desktop-kit/src/shared/i18n/tr.ts similarity index 100% rename from apps/desktop/src/shared/i18n/tr.ts rename to packages/desktop-kit/src/shared/i18n/tr.ts diff --git a/apps/desktop/src/shared/i18n/zh-Hans.ts b/packages/desktop-kit/src/shared/i18n/zh-Hans.ts similarity index 100% rename from apps/desktop/src/shared/i18n/zh-Hans.ts rename to packages/desktop-kit/src/shared/i18n/zh-Hans.ts diff --git a/apps/desktop/src/shared/ipc.ts b/packages/desktop-kit/src/shared/ipc.ts similarity index 100% rename from apps/desktop/src/shared/ipc.ts rename to packages/desktop-kit/src/shared/ipc.ts diff --git a/packages/desktop-kit/test/builder.test.ts b/packages/desktop-kit/test/builder.test.ts new file mode 100644 index 0000000..a12b54e --- /dev/null +++ b/packages/desktop-kit/test/builder.test.ts @@ -0,0 +1,87 @@ +import { mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { MAINNET_ACKNOWLEDGEMENT, defineConfig } from "@clip-wallet/config"; +import { DESKTOP_ICONS, electronBuilderConfig, electronLanguages } from "../src/builder"; +import { walletCsp } from "../src/csp"; +import { desktopBuildValues } from "../src/electron-vite"; +import { stripAppTokens } from "../src/main/browser/browser-ua"; + +const acme = defineConfig({ name: "Acme Wallet", rdns: "com.acme.wallet", languages: ["en", "de", "pt-BR", "zh-Hans"] }); + +describe("electronBuilderConfig: clip.config → electron-builder", () => { + it("names, ids, scheme and icons come from the wallet's config", () => { + const c = electronBuilderConfig({ config: acme, env: {} }); + expect(c.appId).toBe("com.acme.wallet.desktop"); + expect(c.productName).toBe("Acme Wallet"); + expect(c.extraMetadata).toEqual({ name: "acme-wallet-desktop" }); + expect(c.artifactName).toBe("Acme-Wallet-${version}-${os}-${arch}.${ext}"); + expect(c.protocols).toEqual([{ name: "Acme Wallet", schemes: ["acmewallet"] }]); + expect(c.linux.executableName).toBe("acme-wallet"); + expect([c.mac.icon, c.win.icon, c.linux.icon]).toEqual([DESKTOP_ICONS.mac, DESKTOP_ICONS.win, DESKTOP_ICONS.linux]); + expect([c.mac.icon, c.win.icon, c.linux.icon]).toEqual(["build/icon.icns", "build/icon.ico", "build/icons"]); + expect(c.mac.extendInfo.NSCameraUsageDescription).toMatch(/^Acme Wallet uses the camera/); + expect(c.linux.synopsis).toMatch(/Test networks only\.$/); + expect(JSON.stringify(c)).not.toMatch(/Clip Wallet|clipwallet|coldai/i); + }); + + it("app id and scheme overrides, languages, and Chromium locales", () => { + const c = electronBuilderConfig({ config: defineConfig({ ...acme, appId: "com.example.mywallet", scheme: "acme", desktop: { appId: "com.example.desk" } }), env: {} }); + expect(c.appId).toBe("com.example.desk"); + expect(c.protocols[0]!.schemes).toEqual(["acme"]); + expect(c.electronLanguages).toEqual(["en", "de", "pt-BR", "pt_BR", "zh-CN", "zh_CN"]); + expect(electronLanguages(["ja"])).toEqual(["ja", "en"]); + }); + + it("signs, notarizes and publishes only when the environment says so", () => { + const unsigned = electronBuilderConfig({ config: acme, env: {} }); + expect(unsigned.mac).toMatchObject({ identity: null, notarize: false }); + expect(unsigned.publish).toBeNull(); + const signed = electronBuilderConfig({ + config: acme, + env: { CSC_LINK: "x.p12", APPLE_API_KEY: "k.p8", APPLE_API_KEY_ID: "id", APPLE_API_ISSUER: "iss", CLIP_UPDATES: "1", CLIP_UPDATES_OWNER: "acme", CLIP_UPDATES_REPO: "wallet" }, + }); + expect(signed.mac).not.toHaveProperty("identity"); + expect(signed.mac.notarize).toBe(true); + expect(signed.publish).toEqual([{ provider: "github", owner: "acme", repo: "wallet", releaseType: "release" }]); + // CLIP_UPDATES without a repository publishes nothing (no default owner). + expect(electronBuilderConfig({ config: acme, env: { CLIP_UPDATES: "1" } }).publish).toBeNull(); + }); +}); + +describe("clipDesktop build values", () => { + function project(files: Record) { + const dir = mkdtempSync(join(tmpdir(), "clip-desktop-kit-")); + for (const [f, text] of Object.entries(files)) writeFileSync(join(dir, f), text); + return dir; + } + + it("inlines the icon, applies the WalletConnect id from .env, and writes the pages' CSP", () => { + const dir = project({ "icon.svg": "", ".env": "CLIP_WALLETCONNECT_PROJECT_ID=0123456789abcdef0123456789abcdef\nOTHER=x\n" }); + const v = desktopBuildValues({ config: defineConfig({ ...acme, services: { mediaProxyUrl: "https://media.acme.example" } }), root: dir, env: undefined }); + expect(v.icon).toMatch(/^data:image\/svg\+xml;base64,/); + expect(v.config.walletConnect.projectId).toBe("0123456789abcdef0123456789abcdef"); + expect(v.csp).toBe(walletCsp("https://media.acme.example")); + expect(v.csp).toContain("img-src 'self' data: blob: https://media.acme.example"); + }); + + it("refuses a mainnet build while MAINNET.md has open boxes", () => { + const dir = project({ "icon.svg": "", "MAINNET.md": "- [x] audited\n- [ ] keys in a hardware module\n" }); + const config = defineConfig({ + ...acme, + homepage: "https://wallet.acme.example", + extension: { key: "A".repeat(392) }, + walletConnect: { projectId: "0".repeat(32) }, + mainnet: { enabled: true, acknowledged: MAINNET_ACKNOWLEDGEMENT }, + }); + expect(() => desktopBuildValues({ config, root: dir, env: {} })).toThrow(/mainnet checklist: MAINNET\.md: keys in a hardware module/); + }); +}); + +describe("user agent", () => { + it("drops Electron's and the app's own tokens", () => { + const ua = "Mozilla/5.0 (Macintosh) AppleWebKit/537.36 (KHTML, like Gecko) acme-wallet-desktop/0.1.0 Chrome/140.0.0.0 Electron/44.5.1 Safari/537.36"; + expect(stripAppTokens(ua, "Acme Wallet")).toBe("Mozilla/5.0 (Macintosh) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"); + }); +}); diff --git a/packages/desktop-kit/test/clip.config.ts b/packages/desktop-kit/test/clip.config.ts new file mode 100644 index 0000000..c7a6c32 --- /dev/null +++ b/packages/desktop-kit/test/clip.config.ts @@ -0,0 +1,5 @@ +// The config the kit's unit tests run with (vitest aliases virtual:clip-wallet/config here). +import { defineConfig } from "@clip-wallet/config"; + +export default defineConfig({ name: "Test Wallet", rdns: "com.example.testwallet", networks: ["evm:*", "hedera"] }); +export const icon = "data:image/svg+xml;base64,PHN2Zy8+"; diff --git a/apps/desktop/test/i18n.test.ts b/packages/desktop-kit/test/i18n.test.ts similarity index 100% rename from apps/desktop/test/i18n.test.ts rename to packages/desktop-kit/test/i18n.test.ts diff --git a/apps/desktop/test/ipc.test.ts b/packages/desktop-kit/test/ipc.test.ts similarity index 100% rename from apps/desktop/test/ipc.test.ts rename to packages/desktop-kit/test/ipc.test.ts diff --git a/apps/desktop/test/native-hosts.test.ts b/packages/desktop-kit/test/native-hosts.test.ts similarity index 100% rename from apps/desktop/test/native-hosts.test.ts rename to packages/desktop-kit/test/native-hosts.test.ts diff --git a/apps/desktop/test/onemask-relay.test.ts b/packages/desktop-kit/test/onemask-relay.test.ts similarity index 100% rename from apps/desktop/test/onemask-relay.test.ts rename to packages/desktop-kit/test/onemask-relay.test.ts diff --git a/apps/desktop/test/storage.test.ts b/packages/desktop-kit/test/storage.test.ts similarity index 100% rename from apps/desktop/test/storage.test.ts rename to packages/desktop-kit/test/storage.test.ts diff --git a/apps/desktop/test/url-policy.test.ts b/packages/desktop-kit/test/url-policy.test.ts similarity index 100% rename from apps/desktop/test/url-policy.test.ts rename to packages/desktop-kit/test/url-policy.test.ts diff --git a/packages/desktop-kit/tsconfig.json b/packages/desktop-kit/tsconfig.json new file mode 100644 index 0000000..4c02487 --- /dev/null +++ b/packages/desktop-kit/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "types": ["node"], + "noEmit": true + }, + "include": ["src", "test", "vitest.config.ts"] +} diff --git a/packages/desktop-kit/vitest.config.ts b/packages/desktop-kit/vitest.config.ts new file mode 100644 index 0000000..88b33dd --- /dev/null +++ b/packages/desktop-kit/vitest.config.ts @@ -0,0 +1,8 @@ +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + // The kit reads the wallet's config from a virtual module that clipDesktop() provides in a real build. + resolve: { alias: { "virtual:clip-wallet/config": fileURLToPath(new URL("./test/clip.config.ts", import.meta.url)) } }, + test: { include: ["test/**/*.test.ts"], environment: "node" }, +}); diff --git a/packages/i18n/src/locales.ts b/packages/i18n/src/locales.ts index 430d0f2..6a86d06 100644 --- a/packages/i18n/src/locales.ts +++ b/packages/i18n/src/locales.ts @@ -62,23 +62,35 @@ export function dirOf(tag: string): "ltr" | "rtl" { * "zh", "zh-CN", "zh-Hans-SG" → zh-Hans; "zh-TW", "zh-Hant-HK" also → zh-Hans (no Traditional yet; most * Traditional readers read Simplified more easily than English) */ -export function negotiateLocale(requested: readonly (string | undefined | null)[]): LocaleCode { +export function negotiateLocale(requested: readonly (string | undefined | null)[], offered?: readonly LocaleCode[]): LocaleCode { + // A wallet may offer fewer languages (clip.config languages); the first one it offers is its fallback. + const codes: readonly LocaleCode[] = offered?.length ? offered.filter(isLocaleCode) : LOCALE_CODES; + const fallback = offered?.length ? (codes[0] ?? DEFAULT_LOCALE) : DEFAULT_LOCALE; for (const raw of requested) { if (!raw) continue; const tag = raw.replace(/_/g, "-"); - const exact = LOCALE_CODES.find((c) => c.toLowerCase() === tag.toLowerCase()); + const exact = codes.find((c) => c.toLowerCase() === tag.toLowerCase()); if (exact) return exact; const lang = tag.split("-")[0]!.toLowerCase(); - if (lang === "pt") return "pt-BR"; - if (lang === "zh") return "zh-Hans"; - const byLang = LOCALE_CODES.find((c) => c.split("-")[0]!.toLowerCase() === lang); + if (lang === "pt" && codes.includes("pt-BR")) return "pt-BR"; + if (lang === "zh" && codes.includes("zh-Hans")) return "zh-Hans"; + const byLang = codes.find((c) => c.split("-")[0]!.toLowerCase() === lang); if (byLang) return byLang; } - return DEFAULT_LOCALE; + return fallback; +} + +/** The shipped locales a wallet offers (all of them when `offered` is empty or missing), in LOCALES order. */ +export function offeredLocales(offered?: readonly string[]): readonly LocaleInfo[] { + return offered?.length ? LOCALES.filter((l) => offered.includes(l.code)) : LOCALES; } /** The locale to use for a preference: the shipped one chosen, or the device's best match. */ -export function resolveLocale(pref: string | undefined | null, deviceLanguages: readonly (string | undefined | null)[] = []): LocaleCode { - if (isLocaleCode(pref)) return pref; - return negotiateLocale(deviceLanguages); +export function resolveLocale( + pref: string | undefined | null, + deviceLanguages: readonly (string | undefined | null)[] = [], + offered?: readonly LocaleCode[], +): LocaleCode { + if (isLocaleCode(pref) && (!offered?.length || offered.includes(pref))) return pref; + return negotiateLocale(deviceLanguages, offered); } diff --git a/packages/i18n/test/i18n.test.tsx b/packages/i18n/test/i18n.test.tsx index e84a8c2..18a0628 100644 --- a/packages/i18n/test/i18n.test.tsx +++ b/packages/i18n/test/i18n.test.tsx @@ -12,6 +12,7 @@ import { formatPercent, messageArguments, negotiateLocale, + offeredLocales, parseAmountInput, resolveLocale, separators, @@ -34,6 +35,15 @@ describe("locales", () => { expect(resolveLocale("system", ["ko-KR"])).toBe("ko"); expect(resolveLocale("klingon", ["it"])).toBe("it"); }); + it("stays within the languages a wallet offers (clip.config languages); the first offered is the fallback", () => { + expect(negotiateLocale(["fr-CA", "de"], ["en", "de"])).toBe("de"); + expect(negotiateLocale(["pt-PT"], ["es", "en"])).toBe("es"); + expect(negotiateLocale(["sw"], ["ja", "en"])).toBe("ja"); + expect(resolveLocale("ko", ["de"], ["en", "de"])).toBe("de"); + expect(resolveLocale("de", ["en"], ["en", "de"])).toBe("de"); + expect(offeredLocales(["de", "en"]).map((l) => l.code)).toEqual(["en", "de"]); + expect(offeredLocales().length).toBe(12); + }); it("knows right-to-left scripts", () => { expect(dirOf("ar")).toBe("rtl"); expect(dirOf("he-IL")).toBe("rtl"); diff --git a/packages/mobile-kit/.gitignore b/packages/mobile-kit/.gitignore new file mode 100644 index 0000000..e2126be --- /dev/null +++ b/packages/mobile-kit/.gitignore @@ -0,0 +1,5 @@ +dist/ +# Built from @clip-wallet/1mask at typecheck/test/build time. +src/browser/inpage.generated.ts +# Built from @clip-wallet/plugins (SES + runtime) with the inpage bundle. +src/plugins/sandbox.generated.ts diff --git a/packages/mobile-kit/LICENSE b/packages/mobile-kit/LICENSE new file mode 100644 index 0000000..fd2e97a --- /dev/null +++ b/packages/mobile-kit/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 ColdAI (https://coldai.org) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/mobile-kit/NOTICE b/packages/mobile-kit/NOTICE new file mode 100644 index 0000000..7522d21 --- /dev/null +++ b/packages/mobile-kit/NOTICE @@ -0,0 +1,8 @@ +Clip Wallet: @clip-wallet/mobile-kit +Copyright 2026 ColdAI (https://coldai.org) + +This product includes software developed by ColdAI (https://coldai.org). +Licensed under the Apache License, Version 2.0 (see LICENSE). + +"Clip Wallet", "1Mask" and the Clip Wallet logo are trademarks of ColdAI. The Apache +License 2.0 grants no rights to use them (Section 6). diff --git a/packages/mobile-kit/README.md b/packages/mobile-kit/README.md new file mode 100644 index 0000000..67562fd --- /dev/null +++ b/packages/mobile-kit/README.md @@ -0,0 +1,59 @@ +# @clip-wallet/mobile-kit + +The Clip Wallet phone app (Expo / React Native: iOS, Android) as a library. A wallet project keeps its identity +(`clip.config.ts`, `assets/`), `app.config.ts`, `metro.config.js` and a three-line `index.ts`; the screens, the vault and +engine host (Keychain / Keystore, Face ID / fingerprint, passkeys), the in-app dapp browser with 1Mask, WalletConnect, +Clip Plugins, Ledger over Bluetooth and Keystone, linked devices and notifications come from here. + +```ts +// index.ts +import "@clip-wallet/mobile-kit/polyfills"; // first +import { registerClipWallet } from "@clip-wallet/mobile-kit"; +registerClipWallet({ icon: require("./assets/icon.png") }); +``` + +```ts +// app.config.ts +import { expoConfig } from "@clip-wallet/mobile-kit/expo"; +export default () => expoConfig({ configFile: "../../clip.config.ts", root: __dirname }); +``` + +```js +// metro.config.js +const { getDefaultConfig } = require("expo/metro-config"); +const { withClipWallet } = require("@clip-wallet/mobile-kit/metro"); +module.exports = withClipWallet(getDefaultConfig(__dirname), { configFile: "../../clip.config.ts", root: __dirname }); +``` + +`npx create-clip-wallet my-wallet` writes all of this for you (`packages/mobile`). + +## What the kit takes from clip.config.ts + +- **Expo config** (`expoConfig()`): name, slug, deep-link scheme, iOS bundle id and Android package + (`platformIds(config)`: `appId`, else `rdns`; override with `mobile.bundleId` / `mobile.androidPackage`), the icon, + Android adaptive icon (accent background) and splash screen create-clip-wallet renders from the logo, permission + texts in the wallet's name, universal links for `CLIP_ASSOCIATED_DOMAIN`. +- **The app** (`withClipWallet()`): the validated config, with the build environment applied, as the + `virtual:clip-wallet/config` module (written to `node_modules/.cache/clip-wallet/config.js` on every Metro start), so the + screens, the EIP-6963 identity in the in-app browser (name, rdns, icon), the Keychain service (`.vault`), the + languages offered and the hosted services all follow it. +- **Mainnet checklist.** Both refuse a config with mainnet enabled while `mainnetProblems()` lists anything or MAINNET.md + next to clip.config.ts has an open box. + +Native modules are peer dependencies (Expo autolinking only links what the app depends on): install the versions in +this package's `peerDependencies`. Node 22.18 or newer (the helpers read clip.config.ts with Node's type stripping). + +## Building without accounts + +`expo export` (the JS bundle) and `expo prebuild` (the native projects, generated, never committed) need no account. +`eas build` needs an Expo account and, for store builds, Apple Developer / Google Play credentials. + +## Versioning and provenance + +Published from [ColdAI-org/clip-wallet](https://github.com/ColdAI-org/clip-wallet) by CI with npm provenance: every +tarball is signed and traceable to the commit that built it (`npm audit signatures` checks it). All `@clip-wallet/*` +packages share one version; pin it exactly. Pre-release: test networks by default. + +## Licence + +Apache-2.0: see [LICENSE](./LICENSE) and [NOTICE](./NOTICE). "Clip Wallet" and "1Mask" are ColdAI trademarks (not licensed). diff --git a/packages/mobile-kit/babel.config.cjs b/packages/mobile-kit/babel.config.cjs new file mode 100644 index 0000000..b3827bb --- /dev/null +++ b/packages/mobile-kit/babel.config.cjs @@ -0,0 +1,4 @@ +module.exports = function (api) { + api.cache(true); + return { presets: ["babel-preset-expo"] }; +}; diff --git a/apps/mobile/jest.config.js b/packages/mobile-kit/jest.config.cjs similarity index 78% rename from apps/mobile/jest.config.js rename to packages/mobile-kit/jest.config.cjs index e64b102..b4c1d7f 100644 --- a/apps/mobile/jest.config.js +++ b/packages/mobile-kit/jest.config.cjs @@ -4,8 +4,8 @@ module.exports = { testMatch: ["/test/**/*.test.tsx"], // Workspace packages use TypeScript's ".js" import specifiers for ".ts" files. // One React for the app and the workspace packages (pnpm gives packages/i18n its own dev copy). - moduleNameMapper: { "^(\\.{1,2}/.*)\\.js$": "$1", "^react$": "/node_modules/react", "^react/(.*)$": "/node_modules/react/$1" }, - setupFiles: ["/test/jest.setup.js"], + moduleNameMapper: { "^virtual:clip-wallet/config$": "/test/clip.config.ts", "^(\\.{1,2}/.*)\\.js$": "$1", "^react$": "/node_modules/react", "^react/(.*)$": "/node_modules/react/$1" }, + setupFiles: ["/test/jest.setup.cjs"], // Workspace packages export their TypeScript source under the "development" condition (dist/ is for npm). testEnvironmentOptions: { customExportConditions: ["development", "react-native", "require", "default"] }, // pnpm keeps packages under node_modules/.pnpm/@/node_modules/; transform RN, Expo and workspace sources. diff --git a/packages/mobile-kit/package.json b/packages/mobile-kit/package.json new file mode 100644 index 0000000..4396a12 --- /dev/null +++ b/packages/mobile-kit/package.json @@ -0,0 +1,200 @@ +{ + "name": "@clip-wallet/mobile-kit", + "version": "0.1.0", + "description": "Build a branded Clip Wallet phone app (Expo / React Native: iOS, Android): screens, vault host, in-app dapp browser with 1Mask, configured by clip.config.ts", + "keywords": [ + "clip-wallet", + "wallet", + "web3", + "expo", + "react-native" + ], + "license": "Apache-2.0", + "author": "ColdAI (https://coldai.org)", + "homepage": "https://github.com/ColdAI-org/clip-wallet/tree/main/packages/mobile-kit#readme", + "bugs": { + "url": "https://github.com/ColdAI-org/clip-wallet/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ColdAI-org/clip-wallet.git", + "directory": "packages/mobile-kit" + }, + "type": "module", + "sideEffects": [ + "./src/polyfills.ts", + "./src/background/background-task.ts", + "./dist/polyfills.js", + "./dist/background/background-task.js" + ], + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./polyfills": { + "development": "./src/polyfills.ts", + "types": "./dist/polyfills.d.ts", + "import": "./dist/polyfills.js", + "default": "./dist/polyfills.js" + }, + "./expo": { + "development": "./src/expo.ts", + "types": "./dist/expo.d.ts", + "import": "./dist/expo.js", + "default": "./dist/expo.js" + }, + "./metro": { + "development": "./src/metro.cjs", + "default": "./dist/metro.cjs" + }, + "./shims/no-pbkdf2": { + "development": "./src/shims/no-pbkdf2.js", + "default": "./dist/shims/no-pbkdf2.js" + }, + "./shims/node-crypto": { + "development": "./src/shims/node-crypto.js", + "default": "./dist/shims/node-crypto.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "NOTICE" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./polyfills": { + "types": "./dist/polyfills.d.ts", + "import": "./dist/polyfills.js", + "default": "./dist/polyfills.js" + }, + "./expo": { + "types": "./dist/expo.d.ts", + "import": "./dist/expo.js", + "default": "./dist/expo.js" + }, + "./metro": "./dist/metro.cjs", + "./shims/no-pbkdf2": "./dist/shims/no-pbkdf2.js", + "./shims/node-crypto": "./dist/shims/node-crypto.js" + } + }, + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "node scripts/build-inpage.mjs && node ../../tools/release/build-package.mjs", + "inpage": "node scripts/build-inpage.mjs", + "typecheck": "node scripts/build-inpage.mjs && tsc -p . --noEmit", + "test": "node scripts/build-inpage.mjs && vitest run && jest -c jest.config.cjs" + }, + "dependencies": { + "@clip-wallet/1mask": "workspace:*", + "@clip-wallet/config": "workspace:*", + "@clip-wallet/core": "workspace:*", + "@clip-wallet/engine": "workspace:*", + "@clip-wallet/features": "workspace:*", + "@clip-wallet/hardware": "workspace:*", + "@clip-wallet/i18n": "workspace:*", + "@clip-wallet/link": "workspace:*", + "@clip-wallet/names": "workspace:*", + "@clip-wallet/plugins": "workspace:*", + "@clip-wallet/security": "workspace:*", + "@clip-wallet/social": "workspace:*", + "@clip-wallet/ui": "workspace:*", + "@clip-wallet/vault": "workspace:*", + "@noble/hashes": "2.4.0", + "pako": "2.2.0", + "qrcode": "^1.5.4", + "readable-stream": "^3.6.2", + "ses": "2.3.0" + }, + "peerDependencies": { + "@ledgerhq/react-native-hw-transport-ble": "6.41.0", + "@react-native-async-storage/async-storage": "2.2.0", + "@react-native-community/netinfo": "12.0.1", + "@walletconnect/react-native-compat": "2.25.0", + "expo": "~57.0.26", + "expo-application": "~57.0.3", + "expo-background-task": "~57.0.21", + "expo-camera": "~57.0.6", + "expo-clipboard": "~57.0.2", + "expo-crypto": "~57.0.3", + "expo-device": "~57.0.2", + "expo-linking": "~57.0.11", + "expo-local-authentication": "~57.0.3", + "expo-localization": "~57.0.2", + "expo-notifications": "~57.0.21", + "expo-secure-store": "~57.0.4", + "expo-splash-screen": "~57.0.9", + "expo-status-bar": "~57.0.1", + "expo-task-manager": "~57.0.21", + "expo-web-browser": "~57.0.3", + "react": "19.2.3", + "react-native": "0.86.3", + "react-native-argon2": "4.0.0", + "react-native-ble-plx": "3.4.0", + "react-native-get-random-values": "~1.11.0", + "react-native-passkey": "3.6.2", + "react-native-safe-area-context": "~5.7.0", + "react-native-svg": "15.15.4", + "react-native-webview": "13.16.1" + }, + "devDependencies": { + "@ledgerhq/react-native-hw-transport-ble": "6.41.0", + "@react-native-async-storage/async-storage": "2.2.0", + "@react-native-community/netinfo": "12.0.1", + "@react-native/jest-preset": "0.86.3", + "@testing-library/react-native": "^13.3.3", + "@types/jest": "^29.5.14", + "@types/node": "^22.20.5", + "@types/qrcode": "^1.5.6", + "@types/react": "~19.2.0", + "@walletconnect/react-native-compat": "2.25.0", + "esbuild": "^0.25.0", + "expo": "~57.0.26", + "expo-application": "~57.0.3", + "expo-background-task": "~57.0.21", + "expo-camera": "~57.0.6", + "expo-clipboard": "~57.0.2", + "expo-crypto": "~57.0.3", + "expo-dev-client": "~57.0.19", + "expo-device": "~57.0.2", + "expo-linking": "~57.0.11", + "expo-local-authentication": "~57.0.3", + "expo-localization": "~57.0.2", + "expo-notifications": "~57.0.21", + "expo-secure-store": "~57.0.4", + "expo-splash-screen": "~57.0.9", + "expo-status-bar": "~57.0.1", + "expo-task-manager": "~57.0.21", + "expo-web-browser": "~57.0.3", + "happy-dom": "^20.14.5", + "jest": "^29.7.0", + "jest-expo": "~57.0.5", + "react": "19.2.3", + "react-native": "0.86.3", + "react-native-argon2": "4.0.0", + "react-native-ble-plx": "3.4.0", + "react-native-get-random-values": "~1.11.0", + "react-native-passkey": "3.6.2", + "react-native-safe-area-context": "~5.7.0", + "react-native-svg": "15.15.4", + "react-native-webview": "13.16.1", + "react-test-renderer": "19.2.3", + "typescript": "^5.9.3", + "vitest": "~4.1.11" + } +} diff --git a/apps/mobile/scripts/build-inpage.mjs b/packages/mobile-kit/scripts/build-inpage.mjs similarity index 100% rename from apps/mobile/scripts/build-inpage.mjs rename to packages/mobile-kit/scripts/build-inpage.mjs diff --git a/apps/mobile/scripts/build-plugin-sandbox.mjs b/packages/mobile-kit/scripts/build-plugin-sandbox.mjs similarity index 100% rename from apps/mobile/scripts/build-plugin-sandbox.mjs rename to packages/mobile-kit/scripts/build-plugin-sandbox.mjs diff --git a/apps/mobile/src/App.tsx b/packages/mobile-kit/src/App.tsx similarity index 100% rename from apps/mobile/src/App.tsx rename to packages/mobile-kit/src/App.tsx diff --git a/apps/mobile/src/background/argon2.ts b/packages/mobile-kit/src/background/argon2.ts similarity index 100% rename from apps/mobile/src/background/argon2.ts rename to packages/mobile-kit/src/background/argon2.ts diff --git a/apps/mobile/src/background/background-task.ts b/packages/mobile-kit/src/background/background-task.ts similarity index 100% rename from apps/mobile/src/background/background-task.ts rename to packages/mobile-kit/src/background/background-task.ts diff --git a/apps/mobile/src/background/device-key.ts b/packages/mobile-kit/src/background/device-key.ts similarity index 96% rename from apps/mobile/src/background/device-key.ts rename to packages/mobile-kit/src/background/device-key.ts index 282e2af..da54443 100644 --- a/apps/mobile/src/background/device-key.ts +++ b/packages/mobile-kit/src/background/device-key.ts @@ -20,8 +20,10 @@ import { hmac } from "@noble/hashes/hmac.js"; import { sha256 } from "@noble/hashes/sha2.js"; import { ClipError } from "@clip-wallet/core"; import type { PrfProvider } from "@clip-wallet/engine"; +import { APP } from "../env"; -const SERVICE = "org.coldai.clipwallet.devicekey"; +const SERVICE = `${APP.config.rdns}.devicekey`; +const NAME = APP.config.name; function hex(b: Uint8Array): string { let s = ""; @@ -78,10 +80,10 @@ export function deviceKeyPrf(label: string): PrfProvider { const credentialId = getRandomBytes(16); const secret = getRandomBytes(32); const name = `dk.${hex(credentialId)}`; - await SecureStore.setItemAsync(name, hex(secret), opts(`Turn on ${label} for Clip Wallet`)); + await SecureStore.setItemAsync(name, hex(secret), opts(`Turn on ${label} for ${NAME}`)); // Prove it reads back (and that the user can pass the check) before the vault relies on it. await gate(label); - const back = await SecureStore.getItemAsync(name, opts(`Confirm ${label} for Clip Wallet`)); + const back = await SecureStore.getItemAsync(name, opts(`Confirm ${label} for ${NAME}`)); if (back !== hex(secret)) throw new ClipError(`${label} couldn't be set up on this device.`, "biometric/store"); const prfOutput = hmac(sha256, secret, prfInput); secret.fill(0); @@ -91,7 +93,7 @@ export function deviceKeyPrf(label: string): PrfProvider { await gate(label); let stored: string | null; try { - stored = await SecureStore.getItemAsync(`dk.${hex(credentialId)}`, opts(`Unlock Clip Wallet`)); + stored = await SecureStore.getItemAsync(`dk.${hex(credentialId)}`, opts(`Unlock ${NAME}`)); } catch { throw new ClipError(`${label} was cancelled. Use your password instead.`, "biometric/cancelled"); } diff --git a/apps/mobile/src/background/events.ts b/packages/mobile-kit/src/background/events.ts similarity index 100% rename from apps/mobile/src/background/events.ts rename to packages/mobile-kit/src/background/events.ts diff --git a/apps/mobile/src/background/host.ts b/packages/mobile-kit/src/background/host.ts similarity index 100% rename from apps/mobile/src/background/host.ts rename to packages/mobile-kit/src/background/host.ts diff --git a/apps/mobile/src/background/ledger-ble.ts b/packages/mobile-kit/src/background/ledger-ble.ts similarity index 72% rename from apps/mobile/src/background/ledger-ble.ts rename to packages/mobile-kit/src/background/ledger-ble.ts index d7dc449..7ee3087 100644 --- a/apps/mobile/src/background/ledger-ble.ts +++ b/packages/mobile-kit/src/background/ledger-ble.ts @@ -41,13 +41,20 @@ export interface LedgerBle { type BleModule = typeof import("@ledgerhq/react-native-hw-transport-ble").default; -function ble(): BleModule { - try { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require("@ledgerhq/react-native-hw-transport-ble").default as BleModule; - } catch (e) { - throw new ClipError("Bluetooth isn't available in this build of the app.", "hw/no-bluetooth", e); - } +let bleModule: Promise | undefined; +/** + * The BLE transport, loaded on first use (a dynamic import, so a published build keeps it a real dependency the + * bundler sees, and a build without the native module only fails here, in plain words). + */ +function ble(): Promise { + bleModule ??= import("@ledgerhq/react-native-hw-transport-ble").then( + (m) => ((m as { default?: BleModule }).default ?? (m as unknown as BleModule)), + (e: unknown) => { + bleModule = undefined; + throw new ClipError("Bluetooth isn't available in this build of the app.", "hw/no-bluetooth", e); + }, + ); + return bleModule; } async function androidPermissions(): Promise { @@ -84,22 +91,30 @@ export function ledgerBle(kv: KV): LedgerBle { return { async prepare() { await androidPermissions(); - await bluetoothOn(ble()); + await bluetoothOn(await ble()); }, scan(onDevice, onError) { let sub: { unsubscribe(): void } | undefined; - try { - sub = ble().listen({ - next: (e: { type: string; descriptor?: { id: string; localName?: string | null; name?: string | null } }) => { - if (e.type === "add" && e.descriptor) onDevice({ id: e.descriptor.id, name: e.descriptor.localName ?? e.descriptor.name ?? "Ledger" }); - }, - error: () => onError("We couldn't look for your Ledger. Check that Bluetooth is on and try again."), - complete: () => undefined, - }); - } catch (e) { - onError(e instanceof ClipError ? e.userMessage : "Bluetooth isn't available right now."); - } - return () => sub?.unsubscribe(); + let stopped = false; + const fail = (e: unknown) => onError(e instanceof ClipError ? e.userMessage : "Bluetooth isn't available right now."); + void ble().then((m) => { + if (stopped) return; + try { + sub = m.listen({ + next: (e: { type: string; descriptor?: { id: string; localName?: string | null; name?: string | null } }) => { + if (e.type === "add" && e.descriptor) onDevice({ id: e.descriptor.id, name: e.descriptor.localName ?? e.descriptor.name ?? "Ledger" }); + }, + error: () => onError("We couldn't look for your Ledger. Check that Bluetooth is on and try again."), + complete: () => undefined, + }); + } catch (e) { + fail(e); + } + }, fail); + return () => { + stopped = true; + sub?.unsubscribe(); + }; }, async select(d) { await kv.set(KEY, d); @@ -110,12 +125,12 @@ export function ledgerBle(kv: KV): LedgerBle { async forget() { const d = await kv.get(KEY); await kv.set(KEY, null); - if (d) await ble().disconnectDevice(d.id).catch(() => undefined); + if (d) await ble().then((m) => m.disconnectDevice(d.id)).catch(() => undefined); }, transport: async () => { const d = await kv.get(KEY); if (!d) throw new ClipError("Pick your Ledger first: Settings → Hardware wallets → Connect.", "hw/no-device"); - const m = ble(); + const m = await ble(); await bluetoothOn(m); // The hardware package's LedgerConnection maps open/exchange failures to plain words (ledgerError). return (await m.open(d.id)) as unknown as Awaited>; diff --git a/apps/mobile/src/background/link.ts b/packages/mobile-kit/src/background/link.ts similarity index 100% rename from apps/mobile/src/background/link.ts rename to packages/mobile-kit/src/background/link.ts diff --git a/apps/mobile/src/background/notifications.ts b/packages/mobile-kit/src/background/notifications.ts similarity index 100% rename from apps/mobile/src/background/notifications.ts rename to packages/mobile-kit/src/background/notifications.ts diff --git a/apps/mobile/src/background/passkey.ts b/packages/mobile-kit/src/background/passkey.ts similarity index 100% rename from apps/mobile/src/background/passkey.ts rename to packages/mobile-kit/src/background/passkey.ts diff --git a/apps/mobile/src/background/storage.ts b/packages/mobile-kit/src/background/storage.ts similarity index 88% rename from apps/mobile/src/background/storage.ts rename to packages/mobile-kit/src/background/storage.ts index 41d2b1c..b1a4f7b 100644 --- a/apps/mobile/src/background/storage.ts +++ b/packages/mobile-kit/src/background/storage.ts @@ -9,9 +9,11 @@ import * as SecureStore from "expo-secure-store"; import AsyncStorage from "@react-native-async-storage/async-storage"; import type { VaultStorage } from "@clip-wallet/vault"; import { JsonKV, type KV } from "@clip-wallet/engine"; +import { APP } from "../env"; const SECURE_OPTS: SecureStore.SecureStoreOptions = { - keychainService: "org.coldai.clipwallet.vault", + // ".vault" (Clip Wallet: org.coldai.clipwallet.vault): each wallet keeps its own Keychain service. + keychainService: `${APP.config.rdns}.vault`, keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY, }; diff --git a/packages/mobile-kit/src/brand.ts b/packages/mobile-kit/src/brand.ts new file mode 100644 index 0000000..d84919c --- /dev/null +++ b/packages/mobile-kit/src/brand.ts @@ -0,0 +1,16 @@ +/** + * The wallet's own artwork for the screens that show it (onboarding, About). A wallet project passes its bundled + * icon once at start-up (registerClipWallet({ icon: require("./assets/icon.png") })); until then the screens draw no + * image. It is the same 1024 px icon the app store listing uses (create-clip-wallet renders it from the logo). + */ +import type { ImageSourcePropType } from "react-native"; + +let icon: ImageSourcePropType | undefined; + +export function setBrandIcon(source: ImageSourcePropType | undefined): void { + icon = source; +} + +export function brandIcon(): ImageSourcePropType | undefined { + return icon; +} diff --git a/apps/mobile/src/browser/bridge.ts b/packages/mobile-kit/src/browser/bridge.ts similarity index 100% rename from apps/mobile/src/browser/bridge.ts rename to packages/mobile-kit/src/browser/bridge.ts diff --git a/apps/mobile/src/browser/inpage-entry.ts b/packages/mobile-kit/src/browser/inpage-entry.ts similarity index 100% rename from apps/mobile/src/browser/inpage-entry.ts rename to packages/mobile-kit/src/browser/inpage-entry.ts diff --git a/packages/mobile-kit/src/env.ts b/packages/mobile-kit/src/env.ts new file mode 100644 index 0000000..b1ec1e4 --- /dev/null +++ b/packages/mobile-kit/src/env.ts @@ -0,0 +1,41 @@ +/** + * Build-time settings. The wallet's validated clip.config comes from `virtual:clip-wallet/config`, which + * withClipWallet() ("@clip-wallet/mobile-kit/metro") writes from the project's clip.config.ts with the build + * environment applied (and the icon inlined); tests alias it. EXPO_PUBLIC_* values are inlined by Expo's bundler at + * build time; they are public identifiers, never secrets. Nothing here is logged. + */ +import { platformIds, walletKey, type ClipConfig } from "@clip-wallet/config"; +// @ts-ignore: a virtual module, resolved by the host bundler (metro.cjs) or the test runner. +import raw, { icon as rawIcon } from "virtual:clip-wallet/config"; + +const clipConfig: ClipConfig = raw; +const icon: string = rawIcon; +const ids = platformIds(clipConfig); +const wcProjectId = clipConfig.walletConnect.projectId || process.env.EXPO_PUBLIC_WC_PROJECT_ID?.trim() || undefined; +const passkeyRpId = process.env.EXPO_PUBLIC_PASSKEY_RP_ID?.trim() || undefined; +const site = clipConfig.homepage ?? `https://${walletKey(clipConfig)}.example`; +const domain = process.env.CLIP_ASSOCIATED_DOMAIN; + +export const APP = { + config: { ...clipConfig, walletConnect: { ...clipConfig.walletConnect, projectId: wcProjectId } }, + /** Reown project id; WalletConnect is switched off (and the UI says so) without it. */ + wcProjectId, + /** WebAuthn relying party for real passkeys (needs webcredentials association). */ + passkeyRpId, + /** WalletConnect metadata: the wallet's homepage, or a placeholder until it has one. */ + siteUrl: site, + iconUrl: `${site}/icon.png`, + /** EIP-6963 / Wallet Standard identity (icon must be a data URI). */ + identity: { name: clipConfig.name, rdns: clipConfig.rdns, icon }, + /** TON Connect JS bridge key and app name (must match the wallets-list entry). */ + walletKey: walletKey(clipConfig), + /** Deep links: ://wc?uri=…, ://browse?url=… ; universal links once a domain is associated. */ + scheme: ids.scheme, + /** Secure Trade share links: https:///trade#offer=… (universal link) or ://trade#offer=…. */ + tradeLinkBase: domain ? `https://${domain}/trade` : `${ids.scheme}://trade`, + /** + * Google / Apple sign-in for passkey backups returns here (an https universal link on the associated domain, + * listed in the backup service's OIDC_RETURN_URLS). Unset = the buttons stay hidden; email sign-in still works. + */ + backupReturnUrl: domain ? `https://${domain}/backup` : undefined, +} as const; diff --git a/packages/mobile-kit/src/expo.ts b/packages/mobile-kit/src/expo.ts new file mode 100644 index 0000000..0866bef --- /dev/null +++ b/packages/mobile-kit/src/expo.ts @@ -0,0 +1,151 @@ +/** + * Expo's app config for a Clip Wallet phone app, from clip.config.ts (https://docs.expo.dev/workflow/configuration/): + * + * // app.config.ts + * import { expoConfig } from "@clip-wallet/mobile-kit/expo"; + * export default () => expoConfig({ configFile: "../../clip.config.ts", root: __dirname }); + * + * Name, slug, deep-link scheme, iOS bundle id and Android package come from platformIds(config); the icons and the + * splash screen are the files create-clip-wallet renders from the wallet's logo (assets/), with the accent colour as + * the adaptive-icon and splash background. Native projects are generated (`expo prebuild`), never committed. A + * mainnet config is refused until mainnetProblems() is empty and MAINNET.md has no open box. + * + * Optional build-time env (never committed): CLIP_ASSOCIATED_DOMAIN (universal links + passkeys; needs a signed build + * and the domain's AASA / assetlinks files), CLIP_WALLETCONNECT_PROJECT_ID or EXPO_PUBLIC_WC_PROJECT_ID (WalletConnect). + * Node only, synchronous (Expo evaluates app.config.ts as CommonJS). No relative imports: build tools load it from source. + */ +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { isMainnetEnabled, platformIds, type ClipConfig } from "@clip-wallet/config"; +import { buildEnv, loadClipConfigSync, resolveBuildConfig } from "@clip-wallet/config/node"; + +/** The asset files a mobile project carries (rendered from the one logo by create-clip-wallet). */ +export const MOBILE_ASSETS = { + icon: "./assets/icon.png", + adaptiveForeground: "./assets/adaptive-icon.png", + adaptiveMonochrome: "./assets/adaptive-monochrome.png", + splash: "./assets/splash-icon.png", +} as const; + +export interface ExpoConfigOptions { + /** The resolved clip.config (or give configFile). */ + config?: ClipConfig; + /** clip.config.ts, relative to root (read with loadClipConfigSync). Default: clip.config.ts. */ + configFile?: string; + /** The mobile project directory (package.json, assets/, .env). Default: process.cwd(). */ + root?: string; + /** Build environment. Default: process.env plus CLIP_* values from .env in root and next to clip.config.ts. */ + env?: Record; + /** App version. Default: the project's package.json version. */ + version?: string; + /** Extra Expo config merged over the result (ios / android / plugins are merged one level deep). */ + overrides?: Record; +} + +type Dict = Record; + +/** The Expo config (ExpoConfig shape; typed loosely so the kit doesn't pin @expo/config-types). */ +export function expoConfig(options: ExpoConfigOptions = {}): Dict { + const root = resolve(options.root ?? process.cwd()); + const configFile = resolve(root, options.configFile ?? "clip.config.ts"); + const configDir = dirname(configFile); + const env = options.env ?? buildEnv([root, configDir]); + const raw = options.config ?? loadClipConfigSync(configFile); + const config = resolveBuildConfig(raw, env, { checklistDir: configDir, fallbackWcEnv: "EXPO_PUBLIC_WC_PROJECT_ID" }); + const ids = platformIds(config); + const version = options.version ?? readVersion(root); + const domain = env.CLIP_ASSOCIATED_DOMAIN || undefined; + const name = config.name; + const camera = "Scan a connection code from an app, a trade link, or your Keystone's QR codes."; + const bluetooth = "Connect to your Ledger over Bluetooth to add its accounts and approve with it."; + const faceId = `Unlock ${name} with Face ID instead of typing your password.`; + const accent = config.theme.accent; + const monochrome = existsSync(join(root, MOBILE_ASSETS.adaptiveMonochrome)); + + const base: Dict = { + name, + slug: ids.slug, + scheme: ids.scheme, + version, + orientation: "portrait", + icon: MOBILE_ASSETS.icon, + userInterfaceStyle: "automatic", + ios: { + bundleIdentifier: ids.ios.bundleIdentifier, + supportsTablet: false, + ...(domain ? { associatedDomains: [`applinks:${domain}`, `webcredentials:${domain}`] } : {}), + infoPlist: { + NSFaceIDUsageDescription: faceId, + NSCameraUsageDescription: camera, + // Ledger over Bluetooth (react-native-ble-plx). Its config plugin writes the same key; set here so the text is ours. + NSBluetoothAlwaysUsageDescription: bluetooth, + // The in-app browser loads https dapps; plain http only for a dapp served from this computer / LAN while developing. + NSAppTransportSecurity: { NSAllowsArbitraryLoads: false, NSAllowsLocalNetworking: true }, + ITSAppUsesNonExemptEncryption: false, + }, + }, + android: { + package: ids.android.package, + // The logo inside the 66% safe zone on the accent colour; monochrome for Android 13+ themed icons. + adaptiveIcon: { + foregroundImage: MOBILE_ASSETS.adaptiveForeground, + ...(monochrome ? { monochromeImage: MOBILE_ASSETS.adaptiveMonochrome } : {}), + backgroundColor: accent, + }, + allowBackup: false, + // Bluetooth for Ledger: BLUETOOTH_SCAN comes from the ble-plx plugin (with neverForLocation); BLUETOOTH_CONNECT on + // Android 12+, asked at run time; Android 11 and lower need fine location for BLE scans. + permissions: ["android.permission.CAMERA", "android.permission.USE_BIOMETRIC", "android.permission.BLUETOOTH_CONNECT"], + intentFilters: domain + ? [ + { + action: "VIEW", + autoVerify: true, + data: [ + { scheme: "https", host: domain, pathPrefix: "/wc" }, + { scheme: "https", host: domain, pathPrefix: "/trade" }, + ], + category: ["BROWSABLE", "DEFAULT"], + }, + ] + : [], + }, + plugins: [ + // Splash: the logo on the accent colour, same in dark mode. + ["expo-splash-screen", { image: MOBILE_ASSETS.splash, imageWidth: 160, backgroundColor: accent, resizeMode: "contain" }], + ["expo-secure-store", { configureAndroidBackup: true, faceIDPermission: faceId }], + ["expo-local-authentication", { faceIDPermission: faceId }], + ["expo-camera", { cameraPermission: camera, microphonePermission: false, recordAudioAndroid: false }], + // Ledger over Bluetooth. Foreground only (no background modes); neverForLocation: the app never derives location. + ["react-native-ble-plx", { isBackgroundEnabled: false, modes: [], bluetoothAlwaysPermission: bluetooth, neverForLocation: true }], + "expo-web-browser", + // Local notifications only (no push server): adds Android's POST_NOTIFICATIONS; iOS asks at runtime. + "expo-notifications", + // Background checks: UIBackgroundModes "processing" + BGTaskSchedulerPermittedIdentifiers on iOS, WorkManager on Android. + "expo-background-task", + // The phone's language list (Settings → Language: "Match device", within the languages clip.config offers). + "expo-localization", + ], + experiments: { typedRoutes: false }, + extra: { mainnet: isMainnetEnabled(config), clipWallet: { name, rdns: config.rdns, appId: ids.appId, languages: config.languages } }, + }; + return merge(base, options.overrides ?? {}); +} + +function readVersion(root: string): string { + try { + return (JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { version?: string }).version ?? "0.1.0"; + } catch { + return "0.1.0"; + } +} + +/** Overrides win; ios / android / extra objects merge one level deep; arrays are replaced. */ +function merge(base: Dict, over: Dict): Dict { + const out: Dict = { ...base }; + for (const [k, v] of Object.entries(over)) { + const b = base[k]; + out[k] = v && b && typeof v === "object" && typeof b === "object" && !Array.isArray(v) && !Array.isArray(b) ? { ...(b as Dict), ...(v as Dict) } : v; + } + return out; +} diff --git a/apps/mobile/src/i18n/ar/accounts.ts b/packages/mobile-kit/src/i18n/ar/accounts.ts similarity index 100% rename from apps/mobile/src/i18n/ar/accounts.ts rename to packages/mobile-kit/src/i18n/ar/accounts.ts diff --git a/apps/mobile/src/i18n/ar/activity.ts b/packages/mobile-kit/src/i18n/ar/activity.ts similarity index 100% rename from apps/mobile/src/i18n/ar/activity.ts rename to packages/mobile-kit/src/i18n/ar/activity.ts diff --git a/apps/mobile/src/i18n/ar/app.ts b/packages/mobile-kit/src/i18n/ar/app.ts similarity index 100% rename from apps/mobile/src/i18n/ar/app.ts rename to packages/mobile-kit/src/i18n/ar/app.ts diff --git a/apps/mobile/src/i18n/ar/approval.ts b/packages/mobile-kit/src/i18n/ar/approval.ts similarity index 100% rename from apps/mobile/src/i18n/ar/approval.ts rename to packages/mobile-kit/src/i18n/ar/approval.ts diff --git a/apps/mobile/src/i18n/ar/backup.ts b/packages/mobile-kit/src/i18n/ar/backup.ts similarity index 100% rename from apps/mobile/src/i18n/ar/backup.ts rename to packages/mobile-kit/src/i18n/ar/backup.ts diff --git a/apps/mobile/src/i18n/ar/browser.ts b/packages/mobile-kit/src/i18n/ar/browser.ts similarity index 100% rename from apps/mobile/src/i18n/ar/browser.ts rename to packages/mobile-kit/src/i18n/ar/browser.ts diff --git a/apps/mobile/src/i18n/ar/buy.ts b/packages/mobile-kit/src/i18n/ar/buy.ts similarity index 100% rename from apps/mobile/src/i18n/ar/buy.ts rename to packages/mobile-kit/src/i18n/ar/buy.ts diff --git a/apps/mobile/src/i18n/ar/collectibles.ts b/packages/mobile-kit/src/i18n/ar/collectibles.ts similarity index 100% rename from apps/mobile/src/i18n/ar/collectibles.ts rename to packages/mobile-kit/src/i18n/ar/collectibles.ts diff --git a/apps/mobile/src/i18n/ar/common.ts b/packages/mobile-kit/src/i18n/ar/common.ts similarity index 100% rename from apps/mobile/src/i18n/ar/common.ts rename to packages/mobile-kit/src/i18n/ar/common.ts diff --git a/apps/mobile/src/i18n/ar/explore.ts b/packages/mobile-kit/src/i18n/ar/explore.ts similarity index 100% rename from apps/mobile/src/i18n/ar/explore.ts rename to packages/mobile-kit/src/i18n/ar/explore.ts diff --git a/apps/mobile/src/i18n/ar/hardware.ts b/packages/mobile-kit/src/i18n/ar/hardware.ts similarity index 100% rename from apps/mobile/src/i18n/ar/hardware.ts rename to packages/mobile-kit/src/i18n/ar/hardware.ts diff --git a/apps/mobile/src/i18n/ar/home.ts b/packages/mobile-kit/src/i18n/ar/home.ts similarity index 100% rename from apps/mobile/src/i18n/ar/home.ts rename to packages/mobile-kit/src/i18n/ar/home.ts diff --git a/apps/mobile/src/i18n/ar/index.ts b/packages/mobile-kit/src/i18n/ar/index.ts similarity index 100% rename from apps/mobile/src/i18n/ar/index.ts rename to packages/mobile-kit/src/i18n/ar/index.ts diff --git a/apps/mobile/src/i18n/ar/kit.ts b/packages/mobile-kit/src/i18n/ar/kit.ts similarity index 100% rename from apps/mobile/src/i18n/ar/kit.ts rename to packages/mobile-kit/src/i18n/ar/kit.ts diff --git a/apps/mobile/src/i18n/ar/link.ts b/packages/mobile-kit/src/i18n/ar/link.ts similarity index 100% rename from apps/mobile/src/i18n/ar/link.ts rename to packages/mobile-kit/src/i18n/ar/link.ts diff --git a/apps/mobile/src/i18n/ar/onboarding.ts b/packages/mobile-kit/src/i18n/ar/onboarding.ts similarity index 100% rename from apps/mobile/src/i18n/ar/onboarding.ts rename to packages/mobile-kit/src/i18n/ar/onboarding.ts diff --git a/apps/mobile/src/i18n/ar/plugins.ts b/packages/mobile-kit/src/i18n/ar/plugins.ts similarity index 100% rename from apps/mobile/src/i18n/ar/plugins.ts rename to packages/mobile-kit/src/i18n/ar/plugins.ts diff --git a/apps/mobile/src/i18n/ar/receive.ts b/packages/mobile-kit/src/i18n/ar/receive.ts similarity index 100% rename from apps/mobile/src/i18n/ar/receive.ts rename to packages/mobile-kit/src/i18n/ar/receive.ts diff --git a/apps/mobile/src/i18n/ar/scan.ts b/packages/mobile-kit/src/i18n/ar/scan.ts similarity index 100% rename from apps/mobile/src/i18n/ar/scan.ts rename to packages/mobile-kit/src/i18n/ar/scan.ts diff --git a/apps/mobile/src/i18n/ar/security.ts b/packages/mobile-kit/src/i18n/ar/security.ts similarity index 100% rename from apps/mobile/src/i18n/ar/security.ts rename to packages/mobile-kit/src/i18n/ar/security.ts diff --git a/apps/mobile/src/i18n/ar/send.ts b/packages/mobile-kit/src/i18n/ar/send.ts similarity index 100% rename from apps/mobile/src/i18n/ar/send.ts rename to packages/mobile-kit/src/i18n/ar/send.ts diff --git a/apps/mobile/src/i18n/ar/settings.ts b/packages/mobile-kit/src/i18n/ar/settings.ts similarity index 100% rename from apps/mobile/src/i18n/ar/settings.ts rename to packages/mobile-kit/src/i18n/ar/settings.ts diff --git a/apps/mobile/src/i18n/ar/settle.ts b/packages/mobile-kit/src/i18n/ar/settle.ts similarity index 100% rename from apps/mobile/src/i18n/ar/settle.ts rename to packages/mobile-kit/src/i18n/ar/settle.ts diff --git a/apps/mobile/src/i18n/ar/social.ts b/packages/mobile-kit/src/i18n/ar/social.ts similarity index 100% rename from apps/mobile/src/i18n/ar/social.ts rename to packages/mobile-kit/src/i18n/ar/social.ts diff --git a/apps/mobile/src/i18n/ar/stake.ts b/packages/mobile-kit/src/i18n/ar/stake.ts similarity index 100% rename from apps/mobile/src/i18n/ar/stake.ts rename to packages/mobile-kit/src/i18n/ar/stake.ts diff --git a/apps/mobile/src/i18n/ar/swap.ts b/packages/mobile-kit/src/i18n/ar/swap.ts similarity index 100% rename from apps/mobile/src/i18n/ar/swap.ts rename to packages/mobile-kit/src/i18n/ar/swap.ts diff --git a/apps/mobile/src/i18n/ar/trade.ts b/packages/mobile-kit/src/i18n/ar/trade.ts similarity index 100% rename from apps/mobile/src/i18n/ar/trade.ts rename to packages/mobile-kit/src/i18n/ar/trade.ts diff --git a/apps/mobile/src/i18n/de/accounts.ts b/packages/mobile-kit/src/i18n/de/accounts.ts similarity index 100% rename from apps/mobile/src/i18n/de/accounts.ts rename to packages/mobile-kit/src/i18n/de/accounts.ts diff --git a/apps/mobile/src/i18n/de/activity.ts b/packages/mobile-kit/src/i18n/de/activity.ts similarity index 100% rename from apps/mobile/src/i18n/de/activity.ts rename to packages/mobile-kit/src/i18n/de/activity.ts diff --git a/apps/mobile/src/i18n/de/app.ts b/packages/mobile-kit/src/i18n/de/app.ts similarity index 100% rename from apps/mobile/src/i18n/de/app.ts rename to packages/mobile-kit/src/i18n/de/app.ts diff --git a/apps/mobile/src/i18n/de/approval.ts b/packages/mobile-kit/src/i18n/de/approval.ts similarity index 100% rename from apps/mobile/src/i18n/de/approval.ts rename to packages/mobile-kit/src/i18n/de/approval.ts diff --git a/apps/mobile/src/i18n/de/backup.ts b/packages/mobile-kit/src/i18n/de/backup.ts similarity index 100% rename from apps/mobile/src/i18n/de/backup.ts rename to packages/mobile-kit/src/i18n/de/backup.ts diff --git a/apps/mobile/src/i18n/de/browser.ts b/packages/mobile-kit/src/i18n/de/browser.ts similarity index 100% rename from apps/mobile/src/i18n/de/browser.ts rename to packages/mobile-kit/src/i18n/de/browser.ts diff --git a/apps/mobile/src/i18n/de/buy.ts b/packages/mobile-kit/src/i18n/de/buy.ts similarity index 100% rename from apps/mobile/src/i18n/de/buy.ts rename to packages/mobile-kit/src/i18n/de/buy.ts diff --git a/apps/mobile/src/i18n/de/collectibles.ts b/packages/mobile-kit/src/i18n/de/collectibles.ts similarity index 100% rename from apps/mobile/src/i18n/de/collectibles.ts rename to packages/mobile-kit/src/i18n/de/collectibles.ts diff --git a/apps/mobile/src/i18n/de/common.ts b/packages/mobile-kit/src/i18n/de/common.ts similarity index 100% rename from apps/mobile/src/i18n/de/common.ts rename to packages/mobile-kit/src/i18n/de/common.ts diff --git a/apps/mobile/src/i18n/de/explore.ts b/packages/mobile-kit/src/i18n/de/explore.ts similarity index 100% rename from apps/mobile/src/i18n/de/explore.ts rename to packages/mobile-kit/src/i18n/de/explore.ts diff --git a/apps/mobile/src/i18n/de/hardware.ts b/packages/mobile-kit/src/i18n/de/hardware.ts similarity index 100% rename from apps/mobile/src/i18n/de/hardware.ts rename to packages/mobile-kit/src/i18n/de/hardware.ts diff --git a/apps/mobile/src/i18n/de/home.ts b/packages/mobile-kit/src/i18n/de/home.ts similarity index 100% rename from apps/mobile/src/i18n/de/home.ts rename to packages/mobile-kit/src/i18n/de/home.ts diff --git a/apps/mobile/src/i18n/de/index.ts b/packages/mobile-kit/src/i18n/de/index.ts similarity index 100% rename from apps/mobile/src/i18n/de/index.ts rename to packages/mobile-kit/src/i18n/de/index.ts diff --git a/apps/mobile/src/i18n/de/kit.ts b/packages/mobile-kit/src/i18n/de/kit.ts similarity index 100% rename from apps/mobile/src/i18n/de/kit.ts rename to packages/mobile-kit/src/i18n/de/kit.ts diff --git a/apps/mobile/src/i18n/de/link.ts b/packages/mobile-kit/src/i18n/de/link.ts similarity index 100% rename from apps/mobile/src/i18n/de/link.ts rename to packages/mobile-kit/src/i18n/de/link.ts diff --git a/apps/mobile/src/i18n/de/onboarding.ts b/packages/mobile-kit/src/i18n/de/onboarding.ts similarity index 100% rename from apps/mobile/src/i18n/de/onboarding.ts rename to packages/mobile-kit/src/i18n/de/onboarding.ts diff --git a/apps/mobile/src/i18n/de/plugins.ts b/packages/mobile-kit/src/i18n/de/plugins.ts similarity index 100% rename from apps/mobile/src/i18n/de/plugins.ts rename to packages/mobile-kit/src/i18n/de/plugins.ts diff --git a/apps/mobile/src/i18n/de/receive.ts b/packages/mobile-kit/src/i18n/de/receive.ts similarity index 100% rename from apps/mobile/src/i18n/de/receive.ts rename to packages/mobile-kit/src/i18n/de/receive.ts diff --git a/apps/mobile/src/i18n/de/scan.ts b/packages/mobile-kit/src/i18n/de/scan.ts similarity index 100% rename from apps/mobile/src/i18n/de/scan.ts rename to packages/mobile-kit/src/i18n/de/scan.ts diff --git a/apps/mobile/src/i18n/de/security.ts b/packages/mobile-kit/src/i18n/de/security.ts similarity index 100% rename from apps/mobile/src/i18n/de/security.ts rename to packages/mobile-kit/src/i18n/de/security.ts diff --git a/apps/mobile/src/i18n/de/send.ts b/packages/mobile-kit/src/i18n/de/send.ts similarity index 100% rename from apps/mobile/src/i18n/de/send.ts rename to packages/mobile-kit/src/i18n/de/send.ts diff --git a/apps/mobile/src/i18n/de/settings.ts b/packages/mobile-kit/src/i18n/de/settings.ts similarity index 100% rename from apps/mobile/src/i18n/de/settings.ts rename to packages/mobile-kit/src/i18n/de/settings.ts diff --git a/apps/mobile/src/i18n/de/settle.ts b/packages/mobile-kit/src/i18n/de/settle.ts similarity index 100% rename from apps/mobile/src/i18n/de/settle.ts rename to packages/mobile-kit/src/i18n/de/settle.ts diff --git a/apps/mobile/src/i18n/de/social.ts b/packages/mobile-kit/src/i18n/de/social.ts similarity index 100% rename from apps/mobile/src/i18n/de/social.ts rename to packages/mobile-kit/src/i18n/de/social.ts diff --git a/apps/mobile/src/i18n/de/stake.ts b/packages/mobile-kit/src/i18n/de/stake.ts similarity index 100% rename from apps/mobile/src/i18n/de/stake.ts rename to packages/mobile-kit/src/i18n/de/stake.ts diff --git a/apps/mobile/src/i18n/de/swap.ts b/packages/mobile-kit/src/i18n/de/swap.ts similarity index 100% rename from apps/mobile/src/i18n/de/swap.ts rename to packages/mobile-kit/src/i18n/de/swap.ts diff --git a/apps/mobile/src/i18n/de/trade.ts b/packages/mobile-kit/src/i18n/de/trade.ts similarity index 100% rename from apps/mobile/src/i18n/de/trade.ts rename to packages/mobile-kit/src/i18n/de/trade.ts diff --git a/apps/mobile/src/i18n/device.ts b/packages/mobile-kit/src/i18n/device.ts similarity index 100% rename from apps/mobile/src/i18n/device.ts rename to packages/mobile-kit/src/i18n/device.ts diff --git a/apps/mobile/src/i18n/en/accounts.ts b/packages/mobile-kit/src/i18n/en/accounts.ts similarity index 100% rename from apps/mobile/src/i18n/en/accounts.ts rename to packages/mobile-kit/src/i18n/en/accounts.ts diff --git a/apps/mobile/src/i18n/en/activity.ts b/packages/mobile-kit/src/i18n/en/activity.ts similarity index 100% rename from apps/mobile/src/i18n/en/activity.ts rename to packages/mobile-kit/src/i18n/en/activity.ts diff --git a/apps/mobile/src/i18n/en/app.ts b/packages/mobile-kit/src/i18n/en/app.ts similarity index 100% rename from apps/mobile/src/i18n/en/app.ts rename to packages/mobile-kit/src/i18n/en/app.ts diff --git a/apps/mobile/src/i18n/en/approval.ts b/packages/mobile-kit/src/i18n/en/approval.ts similarity index 100% rename from apps/mobile/src/i18n/en/approval.ts rename to packages/mobile-kit/src/i18n/en/approval.ts diff --git a/apps/mobile/src/i18n/en/backup.ts b/packages/mobile-kit/src/i18n/en/backup.ts similarity index 100% rename from apps/mobile/src/i18n/en/backup.ts rename to packages/mobile-kit/src/i18n/en/backup.ts diff --git a/apps/mobile/src/i18n/en/browser.ts b/packages/mobile-kit/src/i18n/en/browser.ts similarity index 100% rename from apps/mobile/src/i18n/en/browser.ts rename to packages/mobile-kit/src/i18n/en/browser.ts diff --git a/apps/mobile/src/i18n/en/buy.ts b/packages/mobile-kit/src/i18n/en/buy.ts similarity index 100% rename from apps/mobile/src/i18n/en/buy.ts rename to packages/mobile-kit/src/i18n/en/buy.ts diff --git a/apps/mobile/src/i18n/en/collectibles.ts b/packages/mobile-kit/src/i18n/en/collectibles.ts similarity index 100% rename from apps/mobile/src/i18n/en/collectibles.ts rename to packages/mobile-kit/src/i18n/en/collectibles.ts diff --git a/apps/mobile/src/i18n/en/common.ts b/packages/mobile-kit/src/i18n/en/common.ts similarity index 100% rename from apps/mobile/src/i18n/en/common.ts rename to packages/mobile-kit/src/i18n/en/common.ts diff --git a/apps/mobile/src/i18n/en/explore.ts b/packages/mobile-kit/src/i18n/en/explore.ts similarity index 100% rename from apps/mobile/src/i18n/en/explore.ts rename to packages/mobile-kit/src/i18n/en/explore.ts diff --git a/apps/mobile/src/i18n/en/hardware.ts b/packages/mobile-kit/src/i18n/en/hardware.ts similarity index 100% rename from apps/mobile/src/i18n/en/hardware.ts rename to packages/mobile-kit/src/i18n/en/hardware.ts diff --git a/apps/mobile/src/i18n/en/home.ts b/packages/mobile-kit/src/i18n/en/home.ts similarity index 100% rename from apps/mobile/src/i18n/en/home.ts rename to packages/mobile-kit/src/i18n/en/home.ts diff --git a/apps/mobile/src/i18n/en/index.ts b/packages/mobile-kit/src/i18n/en/index.ts similarity index 100% rename from apps/mobile/src/i18n/en/index.ts rename to packages/mobile-kit/src/i18n/en/index.ts diff --git a/apps/mobile/src/i18n/en/kit.ts b/packages/mobile-kit/src/i18n/en/kit.ts similarity index 100% rename from apps/mobile/src/i18n/en/kit.ts rename to packages/mobile-kit/src/i18n/en/kit.ts diff --git a/apps/mobile/src/i18n/en/link.ts b/packages/mobile-kit/src/i18n/en/link.ts similarity index 100% rename from apps/mobile/src/i18n/en/link.ts rename to packages/mobile-kit/src/i18n/en/link.ts diff --git a/apps/mobile/src/i18n/en/onboarding.ts b/packages/mobile-kit/src/i18n/en/onboarding.ts similarity index 100% rename from apps/mobile/src/i18n/en/onboarding.ts rename to packages/mobile-kit/src/i18n/en/onboarding.ts diff --git a/apps/mobile/src/i18n/en/plugins.ts b/packages/mobile-kit/src/i18n/en/plugins.ts similarity index 100% rename from apps/mobile/src/i18n/en/plugins.ts rename to packages/mobile-kit/src/i18n/en/plugins.ts diff --git a/apps/mobile/src/i18n/en/receive.ts b/packages/mobile-kit/src/i18n/en/receive.ts similarity index 100% rename from apps/mobile/src/i18n/en/receive.ts rename to packages/mobile-kit/src/i18n/en/receive.ts diff --git a/apps/mobile/src/i18n/en/scan.ts b/packages/mobile-kit/src/i18n/en/scan.ts similarity index 100% rename from apps/mobile/src/i18n/en/scan.ts rename to packages/mobile-kit/src/i18n/en/scan.ts diff --git a/apps/mobile/src/i18n/en/security.ts b/packages/mobile-kit/src/i18n/en/security.ts similarity index 100% rename from apps/mobile/src/i18n/en/security.ts rename to packages/mobile-kit/src/i18n/en/security.ts diff --git a/apps/mobile/src/i18n/en/send.ts b/packages/mobile-kit/src/i18n/en/send.ts similarity index 100% rename from apps/mobile/src/i18n/en/send.ts rename to packages/mobile-kit/src/i18n/en/send.ts diff --git a/apps/mobile/src/i18n/en/settings.ts b/packages/mobile-kit/src/i18n/en/settings.ts similarity index 100% rename from apps/mobile/src/i18n/en/settings.ts rename to packages/mobile-kit/src/i18n/en/settings.ts diff --git a/apps/mobile/src/i18n/en/settle.ts b/packages/mobile-kit/src/i18n/en/settle.ts similarity index 100% rename from apps/mobile/src/i18n/en/settle.ts rename to packages/mobile-kit/src/i18n/en/settle.ts diff --git a/apps/mobile/src/i18n/en/social.ts b/packages/mobile-kit/src/i18n/en/social.ts similarity index 100% rename from apps/mobile/src/i18n/en/social.ts rename to packages/mobile-kit/src/i18n/en/social.ts diff --git a/apps/mobile/src/i18n/en/stake.ts b/packages/mobile-kit/src/i18n/en/stake.ts similarity index 100% rename from apps/mobile/src/i18n/en/stake.ts rename to packages/mobile-kit/src/i18n/en/stake.ts diff --git a/apps/mobile/src/i18n/en/swap.ts b/packages/mobile-kit/src/i18n/en/swap.ts similarity index 100% rename from apps/mobile/src/i18n/en/swap.ts rename to packages/mobile-kit/src/i18n/en/swap.ts diff --git a/apps/mobile/src/i18n/en/trade.ts b/packages/mobile-kit/src/i18n/en/trade.ts similarity index 100% rename from apps/mobile/src/i18n/en/trade.ts rename to packages/mobile-kit/src/i18n/en/trade.ts diff --git a/apps/mobile/src/i18n/es/accounts.ts b/packages/mobile-kit/src/i18n/es/accounts.ts similarity index 100% rename from apps/mobile/src/i18n/es/accounts.ts rename to packages/mobile-kit/src/i18n/es/accounts.ts diff --git a/apps/mobile/src/i18n/es/activity.ts b/packages/mobile-kit/src/i18n/es/activity.ts similarity index 100% rename from apps/mobile/src/i18n/es/activity.ts rename to packages/mobile-kit/src/i18n/es/activity.ts diff --git a/apps/mobile/src/i18n/es/app.ts b/packages/mobile-kit/src/i18n/es/app.ts similarity index 100% rename from apps/mobile/src/i18n/es/app.ts rename to packages/mobile-kit/src/i18n/es/app.ts diff --git a/apps/mobile/src/i18n/es/approval.ts b/packages/mobile-kit/src/i18n/es/approval.ts similarity index 100% rename from apps/mobile/src/i18n/es/approval.ts rename to packages/mobile-kit/src/i18n/es/approval.ts diff --git a/apps/mobile/src/i18n/es/backup.ts b/packages/mobile-kit/src/i18n/es/backup.ts similarity index 100% rename from apps/mobile/src/i18n/es/backup.ts rename to packages/mobile-kit/src/i18n/es/backup.ts diff --git a/apps/mobile/src/i18n/es/browser.ts b/packages/mobile-kit/src/i18n/es/browser.ts similarity index 100% rename from apps/mobile/src/i18n/es/browser.ts rename to packages/mobile-kit/src/i18n/es/browser.ts diff --git a/apps/mobile/src/i18n/es/buy.ts b/packages/mobile-kit/src/i18n/es/buy.ts similarity index 100% rename from apps/mobile/src/i18n/es/buy.ts rename to packages/mobile-kit/src/i18n/es/buy.ts diff --git a/apps/mobile/src/i18n/es/collectibles.ts b/packages/mobile-kit/src/i18n/es/collectibles.ts similarity index 100% rename from apps/mobile/src/i18n/es/collectibles.ts rename to packages/mobile-kit/src/i18n/es/collectibles.ts diff --git a/apps/mobile/src/i18n/es/common.ts b/packages/mobile-kit/src/i18n/es/common.ts similarity index 100% rename from apps/mobile/src/i18n/es/common.ts rename to packages/mobile-kit/src/i18n/es/common.ts diff --git a/apps/mobile/src/i18n/es/explore.ts b/packages/mobile-kit/src/i18n/es/explore.ts similarity index 100% rename from apps/mobile/src/i18n/es/explore.ts rename to packages/mobile-kit/src/i18n/es/explore.ts diff --git a/apps/mobile/src/i18n/es/hardware.ts b/packages/mobile-kit/src/i18n/es/hardware.ts similarity index 100% rename from apps/mobile/src/i18n/es/hardware.ts rename to packages/mobile-kit/src/i18n/es/hardware.ts diff --git a/apps/mobile/src/i18n/es/home.ts b/packages/mobile-kit/src/i18n/es/home.ts similarity index 100% rename from apps/mobile/src/i18n/es/home.ts rename to packages/mobile-kit/src/i18n/es/home.ts diff --git a/apps/mobile/src/i18n/es/index.ts b/packages/mobile-kit/src/i18n/es/index.ts similarity index 100% rename from apps/mobile/src/i18n/es/index.ts rename to packages/mobile-kit/src/i18n/es/index.ts diff --git a/apps/mobile/src/i18n/es/kit.ts b/packages/mobile-kit/src/i18n/es/kit.ts similarity index 100% rename from apps/mobile/src/i18n/es/kit.ts rename to packages/mobile-kit/src/i18n/es/kit.ts diff --git a/apps/mobile/src/i18n/es/link.ts b/packages/mobile-kit/src/i18n/es/link.ts similarity index 100% rename from apps/mobile/src/i18n/es/link.ts rename to packages/mobile-kit/src/i18n/es/link.ts diff --git a/apps/mobile/src/i18n/es/onboarding.ts b/packages/mobile-kit/src/i18n/es/onboarding.ts similarity index 100% rename from apps/mobile/src/i18n/es/onboarding.ts rename to packages/mobile-kit/src/i18n/es/onboarding.ts diff --git a/apps/mobile/src/i18n/es/plugins.ts b/packages/mobile-kit/src/i18n/es/plugins.ts similarity index 100% rename from apps/mobile/src/i18n/es/plugins.ts rename to packages/mobile-kit/src/i18n/es/plugins.ts diff --git a/apps/mobile/src/i18n/es/receive.ts b/packages/mobile-kit/src/i18n/es/receive.ts similarity index 100% rename from apps/mobile/src/i18n/es/receive.ts rename to packages/mobile-kit/src/i18n/es/receive.ts diff --git a/apps/mobile/src/i18n/es/scan.ts b/packages/mobile-kit/src/i18n/es/scan.ts similarity index 100% rename from apps/mobile/src/i18n/es/scan.ts rename to packages/mobile-kit/src/i18n/es/scan.ts diff --git a/apps/mobile/src/i18n/es/security.ts b/packages/mobile-kit/src/i18n/es/security.ts similarity index 100% rename from apps/mobile/src/i18n/es/security.ts rename to packages/mobile-kit/src/i18n/es/security.ts diff --git a/apps/mobile/src/i18n/es/send.ts b/packages/mobile-kit/src/i18n/es/send.ts similarity index 100% rename from apps/mobile/src/i18n/es/send.ts rename to packages/mobile-kit/src/i18n/es/send.ts diff --git a/apps/mobile/src/i18n/es/settings.ts b/packages/mobile-kit/src/i18n/es/settings.ts similarity index 100% rename from apps/mobile/src/i18n/es/settings.ts rename to packages/mobile-kit/src/i18n/es/settings.ts diff --git a/apps/mobile/src/i18n/es/settle.ts b/packages/mobile-kit/src/i18n/es/settle.ts similarity index 100% rename from apps/mobile/src/i18n/es/settle.ts rename to packages/mobile-kit/src/i18n/es/settle.ts diff --git a/apps/mobile/src/i18n/es/social.ts b/packages/mobile-kit/src/i18n/es/social.ts similarity index 100% rename from apps/mobile/src/i18n/es/social.ts rename to packages/mobile-kit/src/i18n/es/social.ts diff --git a/apps/mobile/src/i18n/es/stake.ts b/packages/mobile-kit/src/i18n/es/stake.ts similarity index 100% rename from apps/mobile/src/i18n/es/stake.ts rename to packages/mobile-kit/src/i18n/es/stake.ts diff --git a/apps/mobile/src/i18n/es/swap.ts b/packages/mobile-kit/src/i18n/es/swap.ts similarity index 100% rename from apps/mobile/src/i18n/es/swap.ts rename to packages/mobile-kit/src/i18n/es/swap.ts diff --git a/apps/mobile/src/i18n/es/trade.ts b/packages/mobile-kit/src/i18n/es/trade.ts similarity index 100% rename from apps/mobile/src/i18n/es/trade.ts rename to packages/mobile-kit/src/i18n/es/trade.ts diff --git a/apps/mobile/src/i18n/fr/accounts.ts b/packages/mobile-kit/src/i18n/fr/accounts.ts similarity index 100% rename from apps/mobile/src/i18n/fr/accounts.ts rename to packages/mobile-kit/src/i18n/fr/accounts.ts diff --git a/apps/mobile/src/i18n/fr/activity.ts b/packages/mobile-kit/src/i18n/fr/activity.ts similarity index 100% rename from apps/mobile/src/i18n/fr/activity.ts rename to packages/mobile-kit/src/i18n/fr/activity.ts diff --git a/apps/mobile/src/i18n/fr/app.ts b/packages/mobile-kit/src/i18n/fr/app.ts similarity index 100% rename from apps/mobile/src/i18n/fr/app.ts rename to packages/mobile-kit/src/i18n/fr/app.ts diff --git a/apps/mobile/src/i18n/fr/approval.ts b/packages/mobile-kit/src/i18n/fr/approval.ts similarity index 100% rename from apps/mobile/src/i18n/fr/approval.ts rename to packages/mobile-kit/src/i18n/fr/approval.ts diff --git a/apps/mobile/src/i18n/fr/backup.ts b/packages/mobile-kit/src/i18n/fr/backup.ts similarity index 100% rename from apps/mobile/src/i18n/fr/backup.ts rename to packages/mobile-kit/src/i18n/fr/backup.ts diff --git a/apps/mobile/src/i18n/fr/browser.ts b/packages/mobile-kit/src/i18n/fr/browser.ts similarity index 100% rename from apps/mobile/src/i18n/fr/browser.ts rename to packages/mobile-kit/src/i18n/fr/browser.ts diff --git a/apps/mobile/src/i18n/fr/buy.ts b/packages/mobile-kit/src/i18n/fr/buy.ts similarity index 100% rename from apps/mobile/src/i18n/fr/buy.ts rename to packages/mobile-kit/src/i18n/fr/buy.ts diff --git a/apps/mobile/src/i18n/fr/collectibles.ts b/packages/mobile-kit/src/i18n/fr/collectibles.ts similarity index 100% rename from apps/mobile/src/i18n/fr/collectibles.ts rename to packages/mobile-kit/src/i18n/fr/collectibles.ts diff --git a/apps/mobile/src/i18n/fr/common.ts b/packages/mobile-kit/src/i18n/fr/common.ts similarity index 100% rename from apps/mobile/src/i18n/fr/common.ts rename to packages/mobile-kit/src/i18n/fr/common.ts diff --git a/apps/mobile/src/i18n/fr/explore.ts b/packages/mobile-kit/src/i18n/fr/explore.ts similarity index 100% rename from apps/mobile/src/i18n/fr/explore.ts rename to packages/mobile-kit/src/i18n/fr/explore.ts diff --git a/apps/mobile/src/i18n/fr/hardware.ts b/packages/mobile-kit/src/i18n/fr/hardware.ts similarity index 100% rename from apps/mobile/src/i18n/fr/hardware.ts rename to packages/mobile-kit/src/i18n/fr/hardware.ts diff --git a/apps/mobile/src/i18n/fr/home.ts b/packages/mobile-kit/src/i18n/fr/home.ts similarity index 100% rename from apps/mobile/src/i18n/fr/home.ts rename to packages/mobile-kit/src/i18n/fr/home.ts diff --git a/apps/mobile/src/i18n/fr/index.ts b/packages/mobile-kit/src/i18n/fr/index.ts similarity index 100% rename from apps/mobile/src/i18n/fr/index.ts rename to packages/mobile-kit/src/i18n/fr/index.ts diff --git a/apps/mobile/src/i18n/fr/kit.ts b/packages/mobile-kit/src/i18n/fr/kit.ts similarity index 100% rename from apps/mobile/src/i18n/fr/kit.ts rename to packages/mobile-kit/src/i18n/fr/kit.ts diff --git a/apps/mobile/src/i18n/fr/link.ts b/packages/mobile-kit/src/i18n/fr/link.ts similarity index 100% rename from apps/mobile/src/i18n/fr/link.ts rename to packages/mobile-kit/src/i18n/fr/link.ts diff --git a/apps/mobile/src/i18n/fr/onboarding.ts b/packages/mobile-kit/src/i18n/fr/onboarding.ts similarity index 100% rename from apps/mobile/src/i18n/fr/onboarding.ts rename to packages/mobile-kit/src/i18n/fr/onboarding.ts diff --git a/apps/mobile/src/i18n/fr/plugins.ts b/packages/mobile-kit/src/i18n/fr/plugins.ts similarity index 100% rename from apps/mobile/src/i18n/fr/plugins.ts rename to packages/mobile-kit/src/i18n/fr/plugins.ts diff --git a/apps/mobile/src/i18n/fr/receive.ts b/packages/mobile-kit/src/i18n/fr/receive.ts similarity index 100% rename from apps/mobile/src/i18n/fr/receive.ts rename to packages/mobile-kit/src/i18n/fr/receive.ts diff --git a/apps/mobile/src/i18n/fr/scan.ts b/packages/mobile-kit/src/i18n/fr/scan.ts similarity index 100% rename from apps/mobile/src/i18n/fr/scan.ts rename to packages/mobile-kit/src/i18n/fr/scan.ts diff --git a/apps/mobile/src/i18n/fr/security.ts b/packages/mobile-kit/src/i18n/fr/security.ts similarity index 100% rename from apps/mobile/src/i18n/fr/security.ts rename to packages/mobile-kit/src/i18n/fr/security.ts diff --git a/apps/mobile/src/i18n/fr/send.ts b/packages/mobile-kit/src/i18n/fr/send.ts similarity index 100% rename from apps/mobile/src/i18n/fr/send.ts rename to packages/mobile-kit/src/i18n/fr/send.ts diff --git a/apps/mobile/src/i18n/fr/settings.ts b/packages/mobile-kit/src/i18n/fr/settings.ts similarity index 100% rename from apps/mobile/src/i18n/fr/settings.ts rename to packages/mobile-kit/src/i18n/fr/settings.ts diff --git a/apps/mobile/src/i18n/fr/settle.ts b/packages/mobile-kit/src/i18n/fr/settle.ts similarity index 100% rename from apps/mobile/src/i18n/fr/settle.ts rename to packages/mobile-kit/src/i18n/fr/settle.ts diff --git a/apps/mobile/src/i18n/fr/social.ts b/packages/mobile-kit/src/i18n/fr/social.ts similarity index 100% rename from apps/mobile/src/i18n/fr/social.ts rename to packages/mobile-kit/src/i18n/fr/social.ts diff --git a/apps/mobile/src/i18n/fr/stake.ts b/packages/mobile-kit/src/i18n/fr/stake.ts similarity index 100% rename from apps/mobile/src/i18n/fr/stake.ts rename to packages/mobile-kit/src/i18n/fr/stake.ts diff --git a/apps/mobile/src/i18n/fr/swap.ts b/packages/mobile-kit/src/i18n/fr/swap.ts similarity index 100% rename from apps/mobile/src/i18n/fr/swap.ts rename to packages/mobile-kit/src/i18n/fr/swap.ts diff --git a/apps/mobile/src/i18n/fr/trade.ts b/packages/mobile-kit/src/i18n/fr/trade.ts similarity index 100% rename from apps/mobile/src/i18n/fr/trade.ts rename to packages/mobile-kit/src/i18n/fr/trade.ts diff --git a/apps/mobile/src/i18n/hi/accounts.ts b/packages/mobile-kit/src/i18n/hi/accounts.ts similarity index 100% rename from apps/mobile/src/i18n/hi/accounts.ts rename to packages/mobile-kit/src/i18n/hi/accounts.ts diff --git a/apps/mobile/src/i18n/hi/activity.ts b/packages/mobile-kit/src/i18n/hi/activity.ts similarity index 100% rename from apps/mobile/src/i18n/hi/activity.ts rename to packages/mobile-kit/src/i18n/hi/activity.ts diff --git a/apps/mobile/src/i18n/hi/app.ts b/packages/mobile-kit/src/i18n/hi/app.ts similarity index 100% rename from apps/mobile/src/i18n/hi/app.ts rename to packages/mobile-kit/src/i18n/hi/app.ts diff --git a/apps/mobile/src/i18n/hi/approval.ts b/packages/mobile-kit/src/i18n/hi/approval.ts similarity index 100% rename from apps/mobile/src/i18n/hi/approval.ts rename to packages/mobile-kit/src/i18n/hi/approval.ts diff --git a/apps/mobile/src/i18n/hi/backup.ts b/packages/mobile-kit/src/i18n/hi/backup.ts similarity index 100% rename from apps/mobile/src/i18n/hi/backup.ts rename to packages/mobile-kit/src/i18n/hi/backup.ts diff --git a/apps/mobile/src/i18n/hi/browser.ts b/packages/mobile-kit/src/i18n/hi/browser.ts similarity index 100% rename from apps/mobile/src/i18n/hi/browser.ts rename to packages/mobile-kit/src/i18n/hi/browser.ts diff --git a/apps/mobile/src/i18n/hi/buy.ts b/packages/mobile-kit/src/i18n/hi/buy.ts similarity index 100% rename from apps/mobile/src/i18n/hi/buy.ts rename to packages/mobile-kit/src/i18n/hi/buy.ts diff --git a/apps/mobile/src/i18n/hi/collectibles.ts b/packages/mobile-kit/src/i18n/hi/collectibles.ts similarity index 100% rename from apps/mobile/src/i18n/hi/collectibles.ts rename to packages/mobile-kit/src/i18n/hi/collectibles.ts diff --git a/apps/mobile/src/i18n/hi/common.ts b/packages/mobile-kit/src/i18n/hi/common.ts similarity index 100% rename from apps/mobile/src/i18n/hi/common.ts rename to packages/mobile-kit/src/i18n/hi/common.ts diff --git a/apps/mobile/src/i18n/hi/explore.ts b/packages/mobile-kit/src/i18n/hi/explore.ts similarity index 100% rename from apps/mobile/src/i18n/hi/explore.ts rename to packages/mobile-kit/src/i18n/hi/explore.ts diff --git a/apps/mobile/src/i18n/hi/hardware.ts b/packages/mobile-kit/src/i18n/hi/hardware.ts similarity index 100% rename from apps/mobile/src/i18n/hi/hardware.ts rename to packages/mobile-kit/src/i18n/hi/hardware.ts diff --git a/apps/mobile/src/i18n/hi/home.ts b/packages/mobile-kit/src/i18n/hi/home.ts similarity index 100% rename from apps/mobile/src/i18n/hi/home.ts rename to packages/mobile-kit/src/i18n/hi/home.ts diff --git a/apps/mobile/src/i18n/hi/index.ts b/packages/mobile-kit/src/i18n/hi/index.ts similarity index 100% rename from apps/mobile/src/i18n/hi/index.ts rename to packages/mobile-kit/src/i18n/hi/index.ts diff --git a/apps/mobile/src/i18n/hi/kit.ts b/packages/mobile-kit/src/i18n/hi/kit.ts similarity index 100% rename from apps/mobile/src/i18n/hi/kit.ts rename to packages/mobile-kit/src/i18n/hi/kit.ts diff --git a/apps/mobile/src/i18n/hi/link.ts b/packages/mobile-kit/src/i18n/hi/link.ts similarity index 100% rename from apps/mobile/src/i18n/hi/link.ts rename to packages/mobile-kit/src/i18n/hi/link.ts diff --git a/apps/mobile/src/i18n/hi/onboarding.ts b/packages/mobile-kit/src/i18n/hi/onboarding.ts similarity index 100% rename from apps/mobile/src/i18n/hi/onboarding.ts rename to packages/mobile-kit/src/i18n/hi/onboarding.ts diff --git a/apps/mobile/src/i18n/hi/plugins.ts b/packages/mobile-kit/src/i18n/hi/plugins.ts similarity index 100% rename from apps/mobile/src/i18n/hi/plugins.ts rename to packages/mobile-kit/src/i18n/hi/plugins.ts diff --git a/apps/mobile/src/i18n/hi/receive.ts b/packages/mobile-kit/src/i18n/hi/receive.ts similarity index 100% rename from apps/mobile/src/i18n/hi/receive.ts rename to packages/mobile-kit/src/i18n/hi/receive.ts diff --git a/apps/mobile/src/i18n/hi/scan.ts b/packages/mobile-kit/src/i18n/hi/scan.ts similarity index 100% rename from apps/mobile/src/i18n/hi/scan.ts rename to packages/mobile-kit/src/i18n/hi/scan.ts diff --git a/apps/mobile/src/i18n/hi/security.ts b/packages/mobile-kit/src/i18n/hi/security.ts similarity index 100% rename from apps/mobile/src/i18n/hi/security.ts rename to packages/mobile-kit/src/i18n/hi/security.ts diff --git a/apps/mobile/src/i18n/hi/send.ts b/packages/mobile-kit/src/i18n/hi/send.ts similarity index 100% rename from apps/mobile/src/i18n/hi/send.ts rename to packages/mobile-kit/src/i18n/hi/send.ts diff --git a/apps/mobile/src/i18n/hi/settings.ts b/packages/mobile-kit/src/i18n/hi/settings.ts similarity index 100% rename from apps/mobile/src/i18n/hi/settings.ts rename to packages/mobile-kit/src/i18n/hi/settings.ts diff --git a/apps/mobile/src/i18n/hi/settle.ts b/packages/mobile-kit/src/i18n/hi/settle.ts similarity index 100% rename from apps/mobile/src/i18n/hi/settle.ts rename to packages/mobile-kit/src/i18n/hi/settle.ts diff --git a/apps/mobile/src/i18n/hi/social.ts b/packages/mobile-kit/src/i18n/hi/social.ts similarity index 100% rename from apps/mobile/src/i18n/hi/social.ts rename to packages/mobile-kit/src/i18n/hi/social.ts diff --git a/apps/mobile/src/i18n/hi/stake.ts b/packages/mobile-kit/src/i18n/hi/stake.ts similarity index 100% rename from apps/mobile/src/i18n/hi/stake.ts rename to packages/mobile-kit/src/i18n/hi/stake.ts diff --git a/apps/mobile/src/i18n/hi/swap.ts b/packages/mobile-kit/src/i18n/hi/swap.ts similarity index 100% rename from apps/mobile/src/i18n/hi/swap.ts rename to packages/mobile-kit/src/i18n/hi/swap.ts diff --git a/apps/mobile/src/i18n/hi/trade.ts b/packages/mobile-kit/src/i18n/hi/trade.ts similarity index 100% rename from apps/mobile/src/i18n/hi/trade.ts rename to packages/mobile-kit/src/i18n/hi/trade.ts diff --git a/apps/mobile/src/i18n/index.ts b/packages/mobile-kit/src/i18n/index.ts similarity index 100% rename from apps/mobile/src/i18n/index.ts rename to packages/mobile-kit/src/i18n/index.ts diff --git a/apps/mobile/src/i18n/it/accounts.ts b/packages/mobile-kit/src/i18n/it/accounts.ts similarity index 100% rename from apps/mobile/src/i18n/it/accounts.ts rename to packages/mobile-kit/src/i18n/it/accounts.ts diff --git a/apps/mobile/src/i18n/it/activity.ts b/packages/mobile-kit/src/i18n/it/activity.ts similarity index 100% rename from apps/mobile/src/i18n/it/activity.ts rename to packages/mobile-kit/src/i18n/it/activity.ts diff --git a/apps/mobile/src/i18n/it/app.ts b/packages/mobile-kit/src/i18n/it/app.ts similarity index 100% rename from apps/mobile/src/i18n/it/app.ts rename to packages/mobile-kit/src/i18n/it/app.ts diff --git a/apps/mobile/src/i18n/it/approval.ts b/packages/mobile-kit/src/i18n/it/approval.ts similarity index 100% rename from apps/mobile/src/i18n/it/approval.ts rename to packages/mobile-kit/src/i18n/it/approval.ts diff --git a/apps/mobile/src/i18n/it/backup.ts b/packages/mobile-kit/src/i18n/it/backup.ts similarity index 100% rename from apps/mobile/src/i18n/it/backup.ts rename to packages/mobile-kit/src/i18n/it/backup.ts diff --git a/apps/mobile/src/i18n/it/browser.ts b/packages/mobile-kit/src/i18n/it/browser.ts similarity index 100% rename from apps/mobile/src/i18n/it/browser.ts rename to packages/mobile-kit/src/i18n/it/browser.ts diff --git a/apps/mobile/src/i18n/it/buy.ts b/packages/mobile-kit/src/i18n/it/buy.ts similarity index 100% rename from apps/mobile/src/i18n/it/buy.ts rename to packages/mobile-kit/src/i18n/it/buy.ts diff --git a/apps/mobile/src/i18n/it/collectibles.ts b/packages/mobile-kit/src/i18n/it/collectibles.ts similarity index 100% rename from apps/mobile/src/i18n/it/collectibles.ts rename to packages/mobile-kit/src/i18n/it/collectibles.ts diff --git a/apps/mobile/src/i18n/it/common.ts b/packages/mobile-kit/src/i18n/it/common.ts similarity index 100% rename from apps/mobile/src/i18n/it/common.ts rename to packages/mobile-kit/src/i18n/it/common.ts diff --git a/apps/mobile/src/i18n/it/explore.ts b/packages/mobile-kit/src/i18n/it/explore.ts similarity index 100% rename from apps/mobile/src/i18n/it/explore.ts rename to packages/mobile-kit/src/i18n/it/explore.ts diff --git a/apps/mobile/src/i18n/it/hardware.ts b/packages/mobile-kit/src/i18n/it/hardware.ts similarity index 100% rename from apps/mobile/src/i18n/it/hardware.ts rename to packages/mobile-kit/src/i18n/it/hardware.ts diff --git a/apps/mobile/src/i18n/it/home.ts b/packages/mobile-kit/src/i18n/it/home.ts similarity index 100% rename from apps/mobile/src/i18n/it/home.ts rename to packages/mobile-kit/src/i18n/it/home.ts diff --git a/apps/mobile/src/i18n/it/index.ts b/packages/mobile-kit/src/i18n/it/index.ts similarity index 100% rename from apps/mobile/src/i18n/it/index.ts rename to packages/mobile-kit/src/i18n/it/index.ts diff --git a/apps/mobile/src/i18n/it/kit.ts b/packages/mobile-kit/src/i18n/it/kit.ts similarity index 100% rename from apps/mobile/src/i18n/it/kit.ts rename to packages/mobile-kit/src/i18n/it/kit.ts diff --git a/apps/mobile/src/i18n/it/link.ts b/packages/mobile-kit/src/i18n/it/link.ts similarity index 100% rename from apps/mobile/src/i18n/it/link.ts rename to packages/mobile-kit/src/i18n/it/link.ts diff --git a/apps/mobile/src/i18n/it/onboarding.ts b/packages/mobile-kit/src/i18n/it/onboarding.ts similarity index 100% rename from apps/mobile/src/i18n/it/onboarding.ts rename to packages/mobile-kit/src/i18n/it/onboarding.ts diff --git a/apps/mobile/src/i18n/it/plugins.ts b/packages/mobile-kit/src/i18n/it/plugins.ts similarity index 100% rename from apps/mobile/src/i18n/it/plugins.ts rename to packages/mobile-kit/src/i18n/it/plugins.ts diff --git a/apps/mobile/src/i18n/it/receive.ts b/packages/mobile-kit/src/i18n/it/receive.ts similarity index 100% rename from apps/mobile/src/i18n/it/receive.ts rename to packages/mobile-kit/src/i18n/it/receive.ts diff --git a/apps/mobile/src/i18n/it/scan.ts b/packages/mobile-kit/src/i18n/it/scan.ts similarity index 100% rename from apps/mobile/src/i18n/it/scan.ts rename to packages/mobile-kit/src/i18n/it/scan.ts diff --git a/apps/mobile/src/i18n/it/security.ts b/packages/mobile-kit/src/i18n/it/security.ts similarity index 100% rename from apps/mobile/src/i18n/it/security.ts rename to packages/mobile-kit/src/i18n/it/security.ts diff --git a/apps/mobile/src/i18n/it/send.ts b/packages/mobile-kit/src/i18n/it/send.ts similarity index 100% rename from apps/mobile/src/i18n/it/send.ts rename to packages/mobile-kit/src/i18n/it/send.ts diff --git a/apps/mobile/src/i18n/it/settings.ts b/packages/mobile-kit/src/i18n/it/settings.ts similarity index 100% rename from apps/mobile/src/i18n/it/settings.ts rename to packages/mobile-kit/src/i18n/it/settings.ts diff --git a/apps/mobile/src/i18n/it/settle.ts b/packages/mobile-kit/src/i18n/it/settle.ts similarity index 100% rename from apps/mobile/src/i18n/it/settle.ts rename to packages/mobile-kit/src/i18n/it/settle.ts diff --git a/apps/mobile/src/i18n/it/social.ts b/packages/mobile-kit/src/i18n/it/social.ts similarity index 100% rename from apps/mobile/src/i18n/it/social.ts rename to packages/mobile-kit/src/i18n/it/social.ts diff --git a/apps/mobile/src/i18n/it/stake.ts b/packages/mobile-kit/src/i18n/it/stake.ts similarity index 100% rename from apps/mobile/src/i18n/it/stake.ts rename to packages/mobile-kit/src/i18n/it/stake.ts diff --git a/apps/mobile/src/i18n/it/swap.ts b/packages/mobile-kit/src/i18n/it/swap.ts similarity index 100% rename from apps/mobile/src/i18n/it/swap.ts rename to packages/mobile-kit/src/i18n/it/swap.ts diff --git a/apps/mobile/src/i18n/it/trade.ts b/packages/mobile-kit/src/i18n/it/trade.ts similarity index 100% rename from apps/mobile/src/i18n/it/trade.ts rename to packages/mobile-kit/src/i18n/it/trade.ts diff --git a/apps/mobile/src/i18n/ja/accounts.ts b/packages/mobile-kit/src/i18n/ja/accounts.ts similarity index 100% rename from apps/mobile/src/i18n/ja/accounts.ts rename to packages/mobile-kit/src/i18n/ja/accounts.ts diff --git a/apps/mobile/src/i18n/ja/activity.ts b/packages/mobile-kit/src/i18n/ja/activity.ts similarity index 100% rename from apps/mobile/src/i18n/ja/activity.ts rename to packages/mobile-kit/src/i18n/ja/activity.ts diff --git a/apps/mobile/src/i18n/ja/app.ts b/packages/mobile-kit/src/i18n/ja/app.ts similarity index 100% rename from apps/mobile/src/i18n/ja/app.ts rename to packages/mobile-kit/src/i18n/ja/app.ts diff --git a/apps/mobile/src/i18n/ja/approval.ts b/packages/mobile-kit/src/i18n/ja/approval.ts similarity index 100% rename from apps/mobile/src/i18n/ja/approval.ts rename to packages/mobile-kit/src/i18n/ja/approval.ts diff --git a/apps/mobile/src/i18n/ja/backup.ts b/packages/mobile-kit/src/i18n/ja/backup.ts similarity index 100% rename from apps/mobile/src/i18n/ja/backup.ts rename to packages/mobile-kit/src/i18n/ja/backup.ts diff --git a/apps/mobile/src/i18n/ja/browser.ts b/packages/mobile-kit/src/i18n/ja/browser.ts similarity index 100% rename from apps/mobile/src/i18n/ja/browser.ts rename to packages/mobile-kit/src/i18n/ja/browser.ts diff --git a/apps/mobile/src/i18n/ja/buy.ts b/packages/mobile-kit/src/i18n/ja/buy.ts similarity index 100% rename from apps/mobile/src/i18n/ja/buy.ts rename to packages/mobile-kit/src/i18n/ja/buy.ts diff --git a/apps/mobile/src/i18n/ja/collectibles.ts b/packages/mobile-kit/src/i18n/ja/collectibles.ts similarity index 100% rename from apps/mobile/src/i18n/ja/collectibles.ts rename to packages/mobile-kit/src/i18n/ja/collectibles.ts diff --git a/apps/mobile/src/i18n/ja/common.ts b/packages/mobile-kit/src/i18n/ja/common.ts similarity index 100% rename from apps/mobile/src/i18n/ja/common.ts rename to packages/mobile-kit/src/i18n/ja/common.ts diff --git a/apps/mobile/src/i18n/ja/explore.ts b/packages/mobile-kit/src/i18n/ja/explore.ts similarity index 100% rename from apps/mobile/src/i18n/ja/explore.ts rename to packages/mobile-kit/src/i18n/ja/explore.ts diff --git a/apps/mobile/src/i18n/ja/hardware.ts b/packages/mobile-kit/src/i18n/ja/hardware.ts similarity index 100% rename from apps/mobile/src/i18n/ja/hardware.ts rename to packages/mobile-kit/src/i18n/ja/hardware.ts diff --git a/apps/mobile/src/i18n/ja/home.ts b/packages/mobile-kit/src/i18n/ja/home.ts similarity index 100% rename from apps/mobile/src/i18n/ja/home.ts rename to packages/mobile-kit/src/i18n/ja/home.ts diff --git a/apps/mobile/src/i18n/ja/index.ts b/packages/mobile-kit/src/i18n/ja/index.ts similarity index 100% rename from apps/mobile/src/i18n/ja/index.ts rename to packages/mobile-kit/src/i18n/ja/index.ts diff --git a/apps/mobile/src/i18n/ja/kit.ts b/packages/mobile-kit/src/i18n/ja/kit.ts similarity index 100% rename from apps/mobile/src/i18n/ja/kit.ts rename to packages/mobile-kit/src/i18n/ja/kit.ts diff --git a/apps/mobile/src/i18n/ja/link.ts b/packages/mobile-kit/src/i18n/ja/link.ts similarity index 100% rename from apps/mobile/src/i18n/ja/link.ts rename to packages/mobile-kit/src/i18n/ja/link.ts diff --git a/apps/mobile/src/i18n/ja/onboarding.ts b/packages/mobile-kit/src/i18n/ja/onboarding.ts similarity index 100% rename from apps/mobile/src/i18n/ja/onboarding.ts rename to packages/mobile-kit/src/i18n/ja/onboarding.ts diff --git a/apps/mobile/src/i18n/ja/plugins.ts b/packages/mobile-kit/src/i18n/ja/plugins.ts similarity index 100% rename from apps/mobile/src/i18n/ja/plugins.ts rename to packages/mobile-kit/src/i18n/ja/plugins.ts diff --git a/apps/mobile/src/i18n/ja/receive.ts b/packages/mobile-kit/src/i18n/ja/receive.ts similarity index 100% rename from apps/mobile/src/i18n/ja/receive.ts rename to packages/mobile-kit/src/i18n/ja/receive.ts diff --git a/apps/mobile/src/i18n/ja/scan.ts b/packages/mobile-kit/src/i18n/ja/scan.ts similarity index 100% rename from apps/mobile/src/i18n/ja/scan.ts rename to packages/mobile-kit/src/i18n/ja/scan.ts diff --git a/apps/mobile/src/i18n/ja/security.ts b/packages/mobile-kit/src/i18n/ja/security.ts similarity index 100% rename from apps/mobile/src/i18n/ja/security.ts rename to packages/mobile-kit/src/i18n/ja/security.ts diff --git a/apps/mobile/src/i18n/ja/send.ts b/packages/mobile-kit/src/i18n/ja/send.ts similarity index 100% rename from apps/mobile/src/i18n/ja/send.ts rename to packages/mobile-kit/src/i18n/ja/send.ts diff --git a/apps/mobile/src/i18n/ja/settings.ts b/packages/mobile-kit/src/i18n/ja/settings.ts similarity index 100% rename from apps/mobile/src/i18n/ja/settings.ts rename to packages/mobile-kit/src/i18n/ja/settings.ts diff --git a/apps/mobile/src/i18n/ja/settle.ts b/packages/mobile-kit/src/i18n/ja/settle.ts similarity index 100% rename from apps/mobile/src/i18n/ja/settle.ts rename to packages/mobile-kit/src/i18n/ja/settle.ts diff --git a/apps/mobile/src/i18n/ja/social.ts b/packages/mobile-kit/src/i18n/ja/social.ts similarity index 100% rename from apps/mobile/src/i18n/ja/social.ts rename to packages/mobile-kit/src/i18n/ja/social.ts diff --git a/apps/mobile/src/i18n/ja/stake.ts b/packages/mobile-kit/src/i18n/ja/stake.ts similarity index 100% rename from apps/mobile/src/i18n/ja/stake.ts rename to packages/mobile-kit/src/i18n/ja/stake.ts diff --git a/apps/mobile/src/i18n/ja/swap.ts b/packages/mobile-kit/src/i18n/ja/swap.ts similarity index 100% rename from apps/mobile/src/i18n/ja/swap.ts rename to packages/mobile-kit/src/i18n/ja/swap.ts diff --git a/apps/mobile/src/i18n/ja/trade.ts b/packages/mobile-kit/src/i18n/ja/trade.ts similarity index 100% rename from apps/mobile/src/i18n/ja/trade.ts rename to packages/mobile-kit/src/i18n/ja/trade.ts diff --git a/apps/mobile/src/i18n/ko/accounts.ts b/packages/mobile-kit/src/i18n/ko/accounts.ts similarity index 100% rename from apps/mobile/src/i18n/ko/accounts.ts rename to packages/mobile-kit/src/i18n/ko/accounts.ts diff --git a/apps/mobile/src/i18n/ko/activity.ts b/packages/mobile-kit/src/i18n/ko/activity.ts similarity index 100% rename from apps/mobile/src/i18n/ko/activity.ts rename to packages/mobile-kit/src/i18n/ko/activity.ts diff --git a/apps/mobile/src/i18n/ko/app.ts b/packages/mobile-kit/src/i18n/ko/app.ts similarity index 100% rename from apps/mobile/src/i18n/ko/app.ts rename to packages/mobile-kit/src/i18n/ko/app.ts diff --git a/apps/mobile/src/i18n/ko/approval.ts b/packages/mobile-kit/src/i18n/ko/approval.ts similarity index 100% rename from apps/mobile/src/i18n/ko/approval.ts rename to packages/mobile-kit/src/i18n/ko/approval.ts diff --git a/apps/mobile/src/i18n/ko/backup.ts b/packages/mobile-kit/src/i18n/ko/backup.ts similarity index 100% rename from apps/mobile/src/i18n/ko/backup.ts rename to packages/mobile-kit/src/i18n/ko/backup.ts diff --git a/apps/mobile/src/i18n/ko/browser.ts b/packages/mobile-kit/src/i18n/ko/browser.ts similarity index 100% rename from apps/mobile/src/i18n/ko/browser.ts rename to packages/mobile-kit/src/i18n/ko/browser.ts diff --git a/apps/mobile/src/i18n/ko/buy.ts b/packages/mobile-kit/src/i18n/ko/buy.ts similarity index 100% rename from apps/mobile/src/i18n/ko/buy.ts rename to packages/mobile-kit/src/i18n/ko/buy.ts diff --git a/apps/mobile/src/i18n/ko/collectibles.ts b/packages/mobile-kit/src/i18n/ko/collectibles.ts similarity index 100% rename from apps/mobile/src/i18n/ko/collectibles.ts rename to packages/mobile-kit/src/i18n/ko/collectibles.ts diff --git a/apps/mobile/src/i18n/ko/common.ts b/packages/mobile-kit/src/i18n/ko/common.ts similarity index 100% rename from apps/mobile/src/i18n/ko/common.ts rename to packages/mobile-kit/src/i18n/ko/common.ts diff --git a/apps/mobile/src/i18n/ko/explore.ts b/packages/mobile-kit/src/i18n/ko/explore.ts similarity index 100% rename from apps/mobile/src/i18n/ko/explore.ts rename to packages/mobile-kit/src/i18n/ko/explore.ts diff --git a/apps/mobile/src/i18n/ko/hardware.ts b/packages/mobile-kit/src/i18n/ko/hardware.ts similarity index 100% rename from apps/mobile/src/i18n/ko/hardware.ts rename to packages/mobile-kit/src/i18n/ko/hardware.ts diff --git a/apps/mobile/src/i18n/ko/home.ts b/packages/mobile-kit/src/i18n/ko/home.ts similarity index 100% rename from apps/mobile/src/i18n/ko/home.ts rename to packages/mobile-kit/src/i18n/ko/home.ts diff --git a/apps/mobile/src/i18n/ko/index.ts b/packages/mobile-kit/src/i18n/ko/index.ts similarity index 100% rename from apps/mobile/src/i18n/ko/index.ts rename to packages/mobile-kit/src/i18n/ko/index.ts diff --git a/apps/mobile/src/i18n/ko/kit.ts b/packages/mobile-kit/src/i18n/ko/kit.ts similarity index 100% rename from apps/mobile/src/i18n/ko/kit.ts rename to packages/mobile-kit/src/i18n/ko/kit.ts diff --git a/apps/mobile/src/i18n/ko/link.ts b/packages/mobile-kit/src/i18n/ko/link.ts similarity index 100% rename from apps/mobile/src/i18n/ko/link.ts rename to packages/mobile-kit/src/i18n/ko/link.ts diff --git a/apps/mobile/src/i18n/ko/onboarding.ts b/packages/mobile-kit/src/i18n/ko/onboarding.ts similarity index 100% rename from apps/mobile/src/i18n/ko/onboarding.ts rename to packages/mobile-kit/src/i18n/ko/onboarding.ts diff --git a/apps/mobile/src/i18n/ko/plugins.ts b/packages/mobile-kit/src/i18n/ko/plugins.ts similarity index 100% rename from apps/mobile/src/i18n/ko/plugins.ts rename to packages/mobile-kit/src/i18n/ko/plugins.ts diff --git a/apps/mobile/src/i18n/ko/receive.ts b/packages/mobile-kit/src/i18n/ko/receive.ts similarity index 100% rename from apps/mobile/src/i18n/ko/receive.ts rename to packages/mobile-kit/src/i18n/ko/receive.ts diff --git a/apps/mobile/src/i18n/ko/scan.ts b/packages/mobile-kit/src/i18n/ko/scan.ts similarity index 100% rename from apps/mobile/src/i18n/ko/scan.ts rename to packages/mobile-kit/src/i18n/ko/scan.ts diff --git a/apps/mobile/src/i18n/ko/security.ts b/packages/mobile-kit/src/i18n/ko/security.ts similarity index 100% rename from apps/mobile/src/i18n/ko/security.ts rename to packages/mobile-kit/src/i18n/ko/security.ts diff --git a/apps/mobile/src/i18n/ko/send.ts b/packages/mobile-kit/src/i18n/ko/send.ts similarity index 100% rename from apps/mobile/src/i18n/ko/send.ts rename to packages/mobile-kit/src/i18n/ko/send.ts diff --git a/apps/mobile/src/i18n/ko/settings.ts b/packages/mobile-kit/src/i18n/ko/settings.ts similarity index 100% rename from apps/mobile/src/i18n/ko/settings.ts rename to packages/mobile-kit/src/i18n/ko/settings.ts diff --git a/apps/mobile/src/i18n/ko/settle.ts b/packages/mobile-kit/src/i18n/ko/settle.ts similarity index 100% rename from apps/mobile/src/i18n/ko/settle.ts rename to packages/mobile-kit/src/i18n/ko/settle.ts diff --git a/apps/mobile/src/i18n/ko/social.ts b/packages/mobile-kit/src/i18n/ko/social.ts similarity index 100% rename from apps/mobile/src/i18n/ko/social.ts rename to packages/mobile-kit/src/i18n/ko/social.ts diff --git a/apps/mobile/src/i18n/ko/stake.ts b/packages/mobile-kit/src/i18n/ko/stake.ts similarity index 100% rename from apps/mobile/src/i18n/ko/stake.ts rename to packages/mobile-kit/src/i18n/ko/stake.ts diff --git a/apps/mobile/src/i18n/ko/swap.ts b/packages/mobile-kit/src/i18n/ko/swap.ts similarity index 100% rename from apps/mobile/src/i18n/ko/swap.ts rename to packages/mobile-kit/src/i18n/ko/swap.ts diff --git a/apps/mobile/src/i18n/ko/trade.ts b/packages/mobile-kit/src/i18n/ko/trade.ts similarity index 100% rename from apps/mobile/src/i18n/ko/trade.ts rename to packages/mobile-kit/src/i18n/ko/trade.ts diff --git a/apps/mobile/src/i18n/pt-BR/accounts.ts b/packages/mobile-kit/src/i18n/pt-BR/accounts.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/accounts.ts rename to packages/mobile-kit/src/i18n/pt-BR/accounts.ts diff --git a/apps/mobile/src/i18n/pt-BR/activity.ts b/packages/mobile-kit/src/i18n/pt-BR/activity.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/activity.ts rename to packages/mobile-kit/src/i18n/pt-BR/activity.ts diff --git a/apps/mobile/src/i18n/pt-BR/app.ts b/packages/mobile-kit/src/i18n/pt-BR/app.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/app.ts rename to packages/mobile-kit/src/i18n/pt-BR/app.ts diff --git a/apps/mobile/src/i18n/pt-BR/approval.ts b/packages/mobile-kit/src/i18n/pt-BR/approval.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/approval.ts rename to packages/mobile-kit/src/i18n/pt-BR/approval.ts diff --git a/apps/mobile/src/i18n/pt-BR/backup.ts b/packages/mobile-kit/src/i18n/pt-BR/backup.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/backup.ts rename to packages/mobile-kit/src/i18n/pt-BR/backup.ts diff --git a/apps/mobile/src/i18n/pt-BR/browser.ts b/packages/mobile-kit/src/i18n/pt-BR/browser.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/browser.ts rename to packages/mobile-kit/src/i18n/pt-BR/browser.ts diff --git a/apps/mobile/src/i18n/pt-BR/buy.ts b/packages/mobile-kit/src/i18n/pt-BR/buy.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/buy.ts rename to packages/mobile-kit/src/i18n/pt-BR/buy.ts diff --git a/apps/mobile/src/i18n/pt-BR/collectibles.ts b/packages/mobile-kit/src/i18n/pt-BR/collectibles.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/collectibles.ts rename to packages/mobile-kit/src/i18n/pt-BR/collectibles.ts diff --git a/apps/mobile/src/i18n/pt-BR/common.ts b/packages/mobile-kit/src/i18n/pt-BR/common.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/common.ts rename to packages/mobile-kit/src/i18n/pt-BR/common.ts diff --git a/apps/mobile/src/i18n/pt-BR/explore.ts b/packages/mobile-kit/src/i18n/pt-BR/explore.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/explore.ts rename to packages/mobile-kit/src/i18n/pt-BR/explore.ts diff --git a/apps/mobile/src/i18n/pt-BR/hardware.ts b/packages/mobile-kit/src/i18n/pt-BR/hardware.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/hardware.ts rename to packages/mobile-kit/src/i18n/pt-BR/hardware.ts diff --git a/apps/mobile/src/i18n/pt-BR/home.ts b/packages/mobile-kit/src/i18n/pt-BR/home.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/home.ts rename to packages/mobile-kit/src/i18n/pt-BR/home.ts diff --git a/apps/mobile/src/i18n/pt-BR/index.ts b/packages/mobile-kit/src/i18n/pt-BR/index.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/index.ts rename to packages/mobile-kit/src/i18n/pt-BR/index.ts diff --git a/apps/mobile/src/i18n/pt-BR/kit.ts b/packages/mobile-kit/src/i18n/pt-BR/kit.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/kit.ts rename to packages/mobile-kit/src/i18n/pt-BR/kit.ts diff --git a/apps/mobile/src/i18n/pt-BR/link.ts b/packages/mobile-kit/src/i18n/pt-BR/link.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/link.ts rename to packages/mobile-kit/src/i18n/pt-BR/link.ts diff --git a/apps/mobile/src/i18n/pt-BR/onboarding.ts b/packages/mobile-kit/src/i18n/pt-BR/onboarding.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/onboarding.ts rename to packages/mobile-kit/src/i18n/pt-BR/onboarding.ts diff --git a/apps/mobile/src/i18n/pt-BR/plugins.ts b/packages/mobile-kit/src/i18n/pt-BR/plugins.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/plugins.ts rename to packages/mobile-kit/src/i18n/pt-BR/plugins.ts diff --git a/apps/mobile/src/i18n/pt-BR/receive.ts b/packages/mobile-kit/src/i18n/pt-BR/receive.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/receive.ts rename to packages/mobile-kit/src/i18n/pt-BR/receive.ts diff --git a/apps/mobile/src/i18n/pt-BR/scan.ts b/packages/mobile-kit/src/i18n/pt-BR/scan.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/scan.ts rename to packages/mobile-kit/src/i18n/pt-BR/scan.ts diff --git a/apps/mobile/src/i18n/pt-BR/security.ts b/packages/mobile-kit/src/i18n/pt-BR/security.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/security.ts rename to packages/mobile-kit/src/i18n/pt-BR/security.ts diff --git a/apps/mobile/src/i18n/pt-BR/send.ts b/packages/mobile-kit/src/i18n/pt-BR/send.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/send.ts rename to packages/mobile-kit/src/i18n/pt-BR/send.ts diff --git a/apps/mobile/src/i18n/pt-BR/settings.ts b/packages/mobile-kit/src/i18n/pt-BR/settings.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/settings.ts rename to packages/mobile-kit/src/i18n/pt-BR/settings.ts diff --git a/apps/mobile/src/i18n/pt-BR/settle.ts b/packages/mobile-kit/src/i18n/pt-BR/settle.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/settle.ts rename to packages/mobile-kit/src/i18n/pt-BR/settle.ts diff --git a/apps/mobile/src/i18n/pt-BR/social.ts b/packages/mobile-kit/src/i18n/pt-BR/social.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/social.ts rename to packages/mobile-kit/src/i18n/pt-BR/social.ts diff --git a/apps/mobile/src/i18n/pt-BR/stake.ts b/packages/mobile-kit/src/i18n/pt-BR/stake.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/stake.ts rename to packages/mobile-kit/src/i18n/pt-BR/stake.ts diff --git a/apps/mobile/src/i18n/pt-BR/swap.ts b/packages/mobile-kit/src/i18n/pt-BR/swap.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/swap.ts rename to packages/mobile-kit/src/i18n/pt-BR/swap.ts diff --git a/apps/mobile/src/i18n/pt-BR/trade.ts b/packages/mobile-kit/src/i18n/pt-BR/trade.ts similarity index 100% rename from apps/mobile/src/i18n/pt-BR/trade.ts rename to packages/mobile-kit/src/i18n/pt-BR/trade.ts diff --git a/apps/mobile/src/i18n/tr/accounts.ts b/packages/mobile-kit/src/i18n/tr/accounts.ts similarity index 100% rename from apps/mobile/src/i18n/tr/accounts.ts rename to packages/mobile-kit/src/i18n/tr/accounts.ts diff --git a/apps/mobile/src/i18n/tr/activity.ts b/packages/mobile-kit/src/i18n/tr/activity.ts similarity index 100% rename from apps/mobile/src/i18n/tr/activity.ts rename to packages/mobile-kit/src/i18n/tr/activity.ts diff --git a/apps/mobile/src/i18n/tr/app.ts b/packages/mobile-kit/src/i18n/tr/app.ts similarity index 100% rename from apps/mobile/src/i18n/tr/app.ts rename to packages/mobile-kit/src/i18n/tr/app.ts diff --git a/apps/mobile/src/i18n/tr/approval.ts b/packages/mobile-kit/src/i18n/tr/approval.ts similarity index 100% rename from apps/mobile/src/i18n/tr/approval.ts rename to packages/mobile-kit/src/i18n/tr/approval.ts diff --git a/apps/mobile/src/i18n/tr/backup.ts b/packages/mobile-kit/src/i18n/tr/backup.ts similarity index 100% rename from apps/mobile/src/i18n/tr/backup.ts rename to packages/mobile-kit/src/i18n/tr/backup.ts diff --git a/apps/mobile/src/i18n/tr/browser.ts b/packages/mobile-kit/src/i18n/tr/browser.ts similarity index 100% rename from apps/mobile/src/i18n/tr/browser.ts rename to packages/mobile-kit/src/i18n/tr/browser.ts diff --git a/apps/mobile/src/i18n/tr/buy.ts b/packages/mobile-kit/src/i18n/tr/buy.ts similarity index 100% rename from apps/mobile/src/i18n/tr/buy.ts rename to packages/mobile-kit/src/i18n/tr/buy.ts diff --git a/apps/mobile/src/i18n/tr/collectibles.ts b/packages/mobile-kit/src/i18n/tr/collectibles.ts similarity index 100% rename from apps/mobile/src/i18n/tr/collectibles.ts rename to packages/mobile-kit/src/i18n/tr/collectibles.ts diff --git a/apps/mobile/src/i18n/tr/common.ts b/packages/mobile-kit/src/i18n/tr/common.ts similarity index 100% rename from apps/mobile/src/i18n/tr/common.ts rename to packages/mobile-kit/src/i18n/tr/common.ts diff --git a/apps/mobile/src/i18n/tr/explore.ts b/packages/mobile-kit/src/i18n/tr/explore.ts similarity index 100% rename from apps/mobile/src/i18n/tr/explore.ts rename to packages/mobile-kit/src/i18n/tr/explore.ts diff --git a/apps/mobile/src/i18n/tr/hardware.ts b/packages/mobile-kit/src/i18n/tr/hardware.ts similarity index 100% rename from apps/mobile/src/i18n/tr/hardware.ts rename to packages/mobile-kit/src/i18n/tr/hardware.ts diff --git a/apps/mobile/src/i18n/tr/home.ts b/packages/mobile-kit/src/i18n/tr/home.ts similarity index 100% rename from apps/mobile/src/i18n/tr/home.ts rename to packages/mobile-kit/src/i18n/tr/home.ts diff --git a/apps/mobile/src/i18n/tr/index.ts b/packages/mobile-kit/src/i18n/tr/index.ts similarity index 100% rename from apps/mobile/src/i18n/tr/index.ts rename to packages/mobile-kit/src/i18n/tr/index.ts diff --git a/apps/mobile/src/i18n/tr/kit.ts b/packages/mobile-kit/src/i18n/tr/kit.ts similarity index 100% rename from apps/mobile/src/i18n/tr/kit.ts rename to packages/mobile-kit/src/i18n/tr/kit.ts diff --git a/apps/mobile/src/i18n/tr/link.ts b/packages/mobile-kit/src/i18n/tr/link.ts similarity index 100% rename from apps/mobile/src/i18n/tr/link.ts rename to packages/mobile-kit/src/i18n/tr/link.ts diff --git a/apps/mobile/src/i18n/tr/onboarding.ts b/packages/mobile-kit/src/i18n/tr/onboarding.ts similarity index 100% rename from apps/mobile/src/i18n/tr/onboarding.ts rename to packages/mobile-kit/src/i18n/tr/onboarding.ts diff --git a/apps/mobile/src/i18n/tr/plugins.ts b/packages/mobile-kit/src/i18n/tr/plugins.ts similarity index 100% rename from apps/mobile/src/i18n/tr/plugins.ts rename to packages/mobile-kit/src/i18n/tr/plugins.ts diff --git a/apps/mobile/src/i18n/tr/receive.ts b/packages/mobile-kit/src/i18n/tr/receive.ts similarity index 100% rename from apps/mobile/src/i18n/tr/receive.ts rename to packages/mobile-kit/src/i18n/tr/receive.ts diff --git a/apps/mobile/src/i18n/tr/scan.ts b/packages/mobile-kit/src/i18n/tr/scan.ts similarity index 100% rename from apps/mobile/src/i18n/tr/scan.ts rename to packages/mobile-kit/src/i18n/tr/scan.ts diff --git a/apps/mobile/src/i18n/tr/security.ts b/packages/mobile-kit/src/i18n/tr/security.ts similarity index 100% rename from apps/mobile/src/i18n/tr/security.ts rename to packages/mobile-kit/src/i18n/tr/security.ts diff --git a/apps/mobile/src/i18n/tr/send.ts b/packages/mobile-kit/src/i18n/tr/send.ts similarity index 100% rename from apps/mobile/src/i18n/tr/send.ts rename to packages/mobile-kit/src/i18n/tr/send.ts diff --git a/apps/mobile/src/i18n/tr/settings.ts b/packages/mobile-kit/src/i18n/tr/settings.ts similarity index 100% rename from apps/mobile/src/i18n/tr/settings.ts rename to packages/mobile-kit/src/i18n/tr/settings.ts diff --git a/apps/mobile/src/i18n/tr/settle.ts b/packages/mobile-kit/src/i18n/tr/settle.ts similarity index 100% rename from apps/mobile/src/i18n/tr/settle.ts rename to packages/mobile-kit/src/i18n/tr/settle.ts diff --git a/apps/mobile/src/i18n/tr/social.ts b/packages/mobile-kit/src/i18n/tr/social.ts similarity index 100% rename from apps/mobile/src/i18n/tr/social.ts rename to packages/mobile-kit/src/i18n/tr/social.ts diff --git a/apps/mobile/src/i18n/tr/stake.ts b/packages/mobile-kit/src/i18n/tr/stake.ts similarity index 100% rename from apps/mobile/src/i18n/tr/stake.ts rename to packages/mobile-kit/src/i18n/tr/stake.ts diff --git a/apps/mobile/src/i18n/tr/swap.ts b/packages/mobile-kit/src/i18n/tr/swap.ts similarity index 100% rename from apps/mobile/src/i18n/tr/swap.ts rename to packages/mobile-kit/src/i18n/tr/swap.ts diff --git a/apps/mobile/src/i18n/tr/trade.ts b/packages/mobile-kit/src/i18n/tr/trade.ts similarity index 100% rename from apps/mobile/src/i18n/tr/trade.ts rename to packages/mobile-kit/src/i18n/tr/trade.ts diff --git a/apps/mobile/src/i18n/zh-Hans/accounts.ts b/packages/mobile-kit/src/i18n/zh-Hans/accounts.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/accounts.ts rename to packages/mobile-kit/src/i18n/zh-Hans/accounts.ts diff --git a/apps/mobile/src/i18n/zh-Hans/activity.ts b/packages/mobile-kit/src/i18n/zh-Hans/activity.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/activity.ts rename to packages/mobile-kit/src/i18n/zh-Hans/activity.ts diff --git a/apps/mobile/src/i18n/zh-Hans/app.ts b/packages/mobile-kit/src/i18n/zh-Hans/app.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/app.ts rename to packages/mobile-kit/src/i18n/zh-Hans/app.ts diff --git a/apps/mobile/src/i18n/zh-Hans/approval.ts b/packages/mobile-kit/src/i18n/zh-Hans/approval.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/approval.ts rename to packages/mobile-kit/src/i18n/zh-Hans/approval.ts diff --git a/apps/mobile/src/i18n/zh-Hans/backup.ts b/packages/mobile-kit/src/i18n/zh-Hans/backup.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/backup.ts rename to packages/mobile-kit/src/i18n/zh-Hans/backup.ts diff --git a/apps/mobile/src/i18n/zh-Hans/browser.ts b/packages/mobile-kit/src/i18n/zh-Hans/browser.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/browser.ts rename to packages/mobile-kit/src/i18n/zh-Hans/browser.ts diff --git a/apps/mobile/src/i18n/zh-Hans/buy.ts b/packages/mobile-kit/src/i18n/zh-Hans/buy.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/buy.ts rename to packages/mobile-kit/src/i18n/zh-Hans/buy.ts diff --git a/apps/mobile/src/i18n/zh-Hans/collectibles.ts b/packages/mobile-kit/src/i18n/zh-Hans/collectibles.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/collectibles.ts rename to packages/mobile-kit/src/i18n/zh-Hans/collectibles.ts diff --git a/apps/mobile/src/i18n/zh-Hans/common.ts b/packages/mobile-kit/src/i18n/zh-Hans/common.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/common.ts rename to packages/mobile-kit/src/i18n/zh-Hans/common.ts diff --git a/apps/mobile/src/i18n/zh-Hans/explore.ts b/packages/mobile-kit/src/i18n/zh-Hans/explore.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/explore.ts rename to packages/mobile-kit/src/i18n/zh-Hans/explore.ts diff --git a/apps/mobile/src/i18n/zh-Hans/hardware.ts b/packages/mobile-kit/src/i18n/zh-Hans/hardware.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/hardware.ts rename to packages/mobile-kit/src/i18n/zh-Hans/hardware.ts diff --git a/apps/mobile/src/i18n/zh-Hans/home.ts b/packages/mobile-kit/src/i18n/zh-Hans/home.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/home.ts rename to packages/mobile-kit/src/i18n/zh-Hans/home.ts diff --git a/apps/mobile/src/i18n/zh-Hans/index.ts b/packages/mobile-kit/src/i18n/zh-Hans/index.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/index.ts rename to packages/mobile-kit/src/i18n/zh-Hans/index.ts diff --git a/apps/mobile/src/i18n/zh-Hans/kit.ts b/packages/mobile-kit/src/i18n/zh-Hans/kit.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/kit.ts rename to packages/mobile-kit/src/i18n/zh-Hans/kit.ts diff --git a/apps/mobile/src/i18n/zh-Hans/link.ts b/packages/mobile-kit/src/i18n/zh-Hans/link.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/link.ts rename to packages/mobile-kit/src/i18n/zh-Hans/link.ts diff --git a/apps/mobile/src/i18n/zh-Hans/onboarding.ts b/packages/mobile-kit/src/i18n/zh-Hans/onboarding.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/onboarding.ts rename to packages/mobile-kit/src/i18n/zh-Hans/onboarding.ts diff --git a/apps/mobile/src/i18n/zh-Hans/plugins.ts b/packages/mobile-kit/src/i18n/zh-Hans/plugins.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/plugins.ts rename to packages/mobile-kit/src/i18n/zh-Hans/plugins.ts diff --git a/apps/mobile/src/i18n/zh-Hans/receive.ts b/packages/mobile-kit/src/i18n/zh-Hans/receive.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/receive.ts rename to packages/mobile-kit/src/i18n/zh-Hans/receive.ts diff --git a/apps/mobile/src/i18n/zh-Hans/scan.ts b/packages/mobile-kit/src/i18n/zh-Hans/scan.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/scan.ts rename to packages/mobile-kit/src/i18n/zh-Hans/scan.ts diff --git a/apps/mobile/src/i18n/zh-Hans/security.ts b/packages/mobile-kit/src/i18n/zh-Hans/security.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/security.ts rename to packages/mobile-kit/src/i18n/zh-Hans/security.ts diff --git a/apps/mobile/src/i18n/zh-Hans/send.ts b/packages/mobile-kit/src/i18n/zh-Hans/send.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/send.ts rename to packages/mobile-kit/src/i18n/zh-Hans/send.ts diff --git a/apps/mobile/src/i18n/zh-Hans/settings.ts b/packages/mobile-kit/src/i18n/zh-Hans/settings.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/settings.ts rename to packages/mobile-kit/src/i18n/zh-Hans/settings.ts diff --git a/apps/mobile/src/i18n/zh-Hans/settle.ts b/packages/mobile-kit/src/i18n/zh-Hans/settle.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/settle.ts rename to packages/mobile-kit/src/i18n/zh-Hans/settle.ts diff --git a/apps/mobile/src/i18n/zh-Hans/social.ts b/packages/mobile-kit/src/i18n/zh-Hans/social.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/social.ts rename to packages/mobile-kit/src/i18n/zh-Hans/social.ts diff --git a/apps/mobile/src/i18n/zh-Hans/stake.ts b/packages/mobile-kit/src/i18n/zh-Hans/stake.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/stake.ts rename to packages/mobile-kit/src/i18n/zh-Hans/stake.ts diff --git a/apps/mobile/src/i18n/zh-Hans/swap.ts b/packages/mobile-kit/src/i18n/zh-Hans/swap.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/swap.ts rename to packages/mobile-kit/src/i18n/zh-Hans/swap.ts diff --git a/apps/mobile/src/i18n/zh-Hans/trade.ts b/packages/mobile-kit/src/i18n/zh-Hans/trade.ts similarity index 100% rename from apps/mobile/src/i18n/zh-Hans/trade.ts rename to packages/mobile-kit/src/i18n/zh-Hans/trade.ts diff --git a/packages/mobile-kit/src/index.ts b/packages/mobile-kit/src/index.ts new file mode 100644 index 0000000..57bb980 --- /dev/null +++ b/packages/mobile-kit/src/index.ts @@ -0,0 +1,34 @@ +/** + * @clip-wallet/mobile-kit: the Clip Wallet phone app (Expo / React Native: iOS, Android) as a library. A wallet project + * keeps clip.config.ts, its icons, app.config.ts, metro.config.js and a three-line index.ts; the screens, the vault and + * engine host, the in-app dapp browser with 1Mask, Clip Plugins, hardware wallets and linked devices come from here. + * + * // index.ts + * import "@clip-wallet/mobile-kit/polyfills"; // first: crypto, TextEncoder, URL, Buffer + * import { registerClipWallet } from "@clip-wallet/mobile-kit"; + * registerClipWallet({ icon: require("./assets/icon.png") }); + * + * "@clip-wallet/mobile-kit/expo" expoConfig({ configFile }): app.config.ts from clip.config.ts (Node) + * "@clip-wallet/mobile-kit/metro" withClipWallet(metroConfig, { configFile }): Metro wiring (Node, CommonJS) + */ +// Background notification checks must be defined at load (expo-task-manager). +import "./background/background-task"; +import { createElement } from "react"; +import type { ImageSourcePropType } from "react-native"; +import { registerRootComponent } from "expo"; +import { App } from "./App"; +import { createMobileWallet } from "./background/host"; +import { setBrandIcon } from "./brand"; + +export { App } from "./App"; +export { createMobileWallet, type MobileWallet } from "./background/host"; +export { brandIcon, setBrandIcon } from "./brand"; +export { APP } from "./env"; +export { parseDeepLink, type DeepLink } from "./lib/deeplinks"; + +/** Start the app: the wallet host, then the root component. `icon` is the wallet's bundled app icon (onboarding). */ +export function registerClipWallet(options: { icon?: ImageSourcePropType } = {}): void { + setBrandIcon(options.icon); + const wallet = createMobileWallet(); + registerRootComponent(() => createElement(App, { wallet })); +} diff --git a/apps/mobile/src/lib/deeplinks.ts b/packages/mobile-kit/src/lib/deeplinks.ts similarity index 100% rename from apps/mobile/src/lib/deeplinks.ts rename to packages/mobile-kit/src/lib/deeplinks.ts diff --git a/apps/mobile/src/lib/family-label.ts b/packages/mobile-kit/src/lib/family-label.ts similarity index 100% rename from apps/mobile/src/lib/family-label.ts rename to packages/mobile-kit/src/lib/family-label.ts diff --git a/apps/mobile/src/lib/notice-routes.ts b/packages/mobile-kit/src/lib/notice-routes.ts similarity index 100% rename from apps/mobile/src/lib/notice-routes.ts rename to packages/mobile-kit/src/lib/notice-routes.ts diff --git a/apps/mobile/src/lib/security-text.ts b/packages/mobile-kit/src/lib/security-text.ts similarity index 100% rename from apps/mobile/src/lib/security-text.ts rename to packages/mobile-kit/src/lib/security-text.ts diff --git a/apps/mobile/src/lib/social-errors.ts b/packages/mobile-kit/src/lib/social-errors.ts similarity index 100% rename from apps/mobile/src/lib/social-errors.ts rename to packages/mobile-kit/src/lib/social-errors.ts diff --git a/apps/mobile/src/lib/swap-links.ts b/packages/mobile-kit/src/lib/swap-links.ts similarity index 100% rename from apps/mobile/src/lib/swap-links.ts rename to packages/mobile-kit/src/lib/swap-links.ts diff --git a/apps/mobile/src/lib/trade-text.ts b/packages/mobile-kit/src/lib/trade-text.ts similarity index 100% rename from apps/mobile/src/lib/trade-text.ts rename to packages/mobile-kit/src/lib/trade-text.ts diff --git a/packages/mobile-kit/src/metro.cjs b/packages/mobile-kit/src/metro.cjs new file mode 100644 index 0000000..8928d7f --- /dev/null +++ b/packages/mobile-kit/src/metro.cjs @@ -0,0 +1,82 @@ +// Metro wiring for a Clip Wallet phone app (CommonJS: metro.config.js requires it). +// +// // metro.config.js +// const { getDefaultConfig } = require("expo/metro-config"); +// const { withClipWallet } = require("@clip-wallet/mobile-kit/metro"); +// module.exports = withClipWallet(getDefaultConfig(__dirname), { configFile: "../../clip.config.ts", root: __dirname }); +// +// What it adds: +// - virtual:clip-wallet/config: the project's clip.config.ts, evaluated and validated in Node with the build +// environment applied (WalletConnect id, mainnet checklist), plus the icon as a data: URI. Written to +// node_modules/.cache/clip-wallet/config.js on every Metro start; the app never evaluates clip.config itself. +// - shims for Node modules that dependencies require on code paths the wallet never runs (crypto, stream, pbkdf2); +// - one React for the app and every package (hooks from a second copy crash); +// - the "development" export condition and TypeScript's ".js → .ts" imports for workspace packages (monorepo only). +// Needs Node 22.18+ (it loads ESM helpers from @clip-wallet/config/node and clip.config.ts with type stripping). +const { mkdirSync, writeFileSync } = require("node:fs"); +const path = require("node:path"); + +/** The resolved config and icon for the app bundle (exported for tests). */ +function configModule(options) { + const { buildEnv, iconDataUri, loadClipConfigSync, resolveBuildConfig } = require("@clip-wallet/config/node"); + const root = path.resolve(options.root || process.cwd()); + const configFile = path.resolve(root, options.configFile || "clip.config.ts"); + const configDir = path.dirname(configFile); + const env = options.env || buildEnv([root, configDir]); + const config = resolveBuildConfig(loadClipConfigSync(configFile), env, { checklistDir: configDir, fallbackWcEnv: "EXPO_PUBLIC_WC_PROJECT_ID" }); + const icon = iconDataUri(configDir, config.icon); + return { + config, + icon, + source: + "// GENERATED by @clip-wallet/mobile-kit/metro from clip.config.ts on every Metro start. Do not edit.\n" + + `Object.defineProperty(exports, "__esModule", { value: true });\nexports.default = ${JSON.stringify(config)};\nexports.icon = ${JSON.stringify(icon)};\n`, + }; +} + +function withClipWallet(metroConfig, options = {}) { + const root = path.resolve(options.root || metroConfig.projectRoot || process.cwd()); + const { source } = configModule({ ...options, root }); + const generated = path.join(root, "node_modules", ".cache", "clip-wallet", "config.js"); + mkdirSync(path.dirname(generated), { recursive: true }); + writeFileSync(generated, source); + + const shims = { + "react-native-fast-pbkdf2": path.join(__dirname, "shims", "no-pbkdf2.js"), + // Node's crypto for hdkey (Keystone SDK → bc-ur-registry-eth), backed by @noble/hashes. + crypto: path.join(__dirname, "shims", "node-crypto.js"), + // Node's stream for cipher-base (create-hash → bs58check → @keystonehq/bc-ur-registry): the browser build of + // readable-stream, which is what bundlers for the web use for the same package. + stream: require.resolve("readable-stream/readable-browser.js"), + }; + + const config = { ...metroConfig, resolver: { ...metroConfig.resolver } }; + // Workspace packages export their TypeScript source under the "development" condition (dist/ is for npm). + config.resolver.unstable_conditionNames = ["development", ...(metroConfig.resolver?.unstable_conditionNames ?? ["require", "import"])]; + const upstream = metroConfig.resolver?.resolveRequest; + config.resolver.resolveRequest = (context, moduleName, platform) => { + const resolve = upstream ?? context.resolveRequest; + if (moduleName === "virtual:clip-wallet/config") return { type: "sourceFile", filePath: generated }; + if (shims[moduleName]) return { type: "sourceFile", filePath: shims[moduleName] }; + // One React: packages with React hooks may have their own dev copy of react under pnpm; always use the app's. + if (moduleName === "react" || moduleName.startsWith("react/")) { + return resolve({ ...context, originModulePath: path.join(root, "package.json") }, moduleName, platform); + } + if (moduleName.startsWith(".") && moduleName.endsWith(".js")) { + const from = path.dirname(context.originModulePath); + if (from.includes(`${path.sep}packages${path.sep}`) && !from.includes(`${path.sep}node_modules${path.sep}`)) { + for (const ext of [".ts", ".tsx"]) { + try { + return resolve(context, moduleName.slice(0, -3) + ext, platform); + } catch { + /* try next */ + } + } + } + } + return resolve(context, moduleName, platform); + }; + return config; +} + +module.exports = { withClipWallet, configModule }; diff --git a/apps/mobile/src/plugins/PluginSandboxes.tsx b/packages/mobile-kit/src/plugins/PluginSandboxes.tsx similarity index 100% rename from apps/mobile/src/plugins/PluginSandboxes.tsx rename to packages/mobile-kit/src/plugins/PluginSandboxes.tsx diff --git a/apps/mobile/src/plugins/channels.ts b/packages/mobile-kit/src/plugins/channels.ts similarity index 100% rename from apps/mobile/src/plugins/channels.ts rename to packages/mobile-kit/src/plugins/channels.ts diff --git a/apps/mobile/src/plugins/gunzip.ts b/packages/mobile-kit/src/plugins/gunzip.ts similarity index 100% rename from apps/mobile/src/plugins/gunzip.ts rename to packages/mobile-kit/src/plugins/gunzip.ts diff --git a/apps/mobile/src/plugins/host.ts b/packages/mobile-kit/src/plugins/host.ts similarity index 100% rename from apps/mobile/src/plugins/host.ts rename to packages/mobile-kit/src/plugins/host.ts diff --git a/apps/mobile/src/plugins/pako.d.ts b/packages/mobile-kit/src/plugins/pako.d.ts similarity index 100% rename from apps/mobile/src/plugins/pako.d.ts rename to packages/mobile-kit/src/plugins/pako.d.ts diff --git a/apps/mobile/src/plugins/protocol.ts b/packages/mobile-kit/src/plugins/protocol.ts similarity index 100% rename from apps/mobile/src/plugins/protocol.ts rename to packages/mobile-kit/src/plugins/protocol.ts diff --git a/apps/mobile/src/plugins/sandbox-entry.ts b/packages/mobile-kit/src/plugins/sandbox-entry.ts similarity index 100% rename from apps/mobile/src/plugins/sandbox-entry.ts rename to packages/mobile-kit/src/plugins/sandbox-entry.ts diff --git a/apps/mobile/src/polyfills.ts b/packages/mobile-kit/src/polyfills.ts similarity index 100% rename from apps/mobile/src/polyfills.ts rename to packages/mobile-kit/src/polyfills.ts diff --git a/apps/mobile/src/screens/Accounts.tsx b/packages/mobile-kit/src/screens/Accounts.tsx similarity index 100% rename from apps/mobile/src/screens/Accounts.tsx rename to packages/mobile-kit/src/screens/Accounts.tsx diff --git a/apps/mobile/src/screens/Activity.tsx b/packages/mobile-kit/src/screens/Activity.tsx similarity index 100% rename from apps/mobile/src/screens/Activity.tsx rename to packages/mobile-kit/src/screens/Activity.tsx diff --git a/apps/mobile/src/screens/Approval.tsx b/packages/mobile-kit/src/screens/Approval.tsx similarity index 100% rename from apps/mobile/src/screens/Approval.tsx rename to packages/mobile-kit/src/screens/Approval.tsx diff --git a/apps/mobile/src/screens/Backup.tsx b/packages/mobile-kit/src/screens/Backup.tsx similarity index 100% rename from apps/mobile/src/screens/Backup.tsx rename to packages/mobile-kit/src/screens/Backup.tsx diff --git a/apps/mobile/src/screens/Browser.tsx b/packages/mobile-kit/src/screens/Browser.tsx similarity index 100% rename from apps/mobile/src/screens/Browser.tsx rename to packages/mobile-kit/src/screens/Browser.tsx diff --git a/apps/mobile/src/screens/Buy.tsx b/packages/mobile-kit/src/screens/Buy.tsx similarity index 100% rename from apps/mobile/src/screens/Buy.tsx rename to packages/mobile-kit/src/screens/Buy.tsx diff --git a/apps/mobile/src/screens/Collectibles.tsx b/packages/mobile-kit/src/screens/Collectibles.tsx similarity index 100% rename from apps/mobile/src/screens/Collectibles.tsx rename to packages/mobile-kit/src/screens/Collectibles.tsx diff --git a/apps/mobile/src/screens/ContactEdit.tsx b/packages/mobile-kit/src/screens/ContactEdit.tsx similarity index 100% rename from apps/mobile/src/screens/ContactEdit.tsx rename to packages/mobile-kit/src/screens/ContactEdit.tsx diff --git a/apps/mobile/src/screens/Contacts.tsx b/packages/mobile-kit/src/screens/Contacts.tsx similarity index 100% rename from apps/mobile/src/screens/Contacts.tsx rename to packages/mobile-kit/src/screens/Contacts.tsx diff --git a/apps/mobile/src/screens/DataUse.tsx b/packages/mobile-kit/src/screens/DataUse.tsx similarity index 100% rename from apps/mobile/src/screens/DataUse.tsx rename to packages/mobile-kit/src/screens/DataUse.tsx diff --git a/apps/mobile/src/screens/Explore.tsx b/packages/mobile-kit/src/screens/Explore.tsx similarity index 100% rename from apps/mobile/src/screens/Explore.tsx rename to packages/mobile-kit/src/screens/Explore.tsx diff --git a/apps/mobile/src/screens/Hardware.tsx b/packages/mobile-kit/src/screens/Hardware.tsx similarity index 100% rename from apps/mobile/src/screens/Hardware.tsx rename to packages/mobile-kit/src/screens/Hardware.tsx diff --git a/apps/mobile/src/screens/Home.tsx b/packages/mobile-kit/src/screens/Home.tsx similarity index 100% rename from apps/mobile/src/screens/Home.tsx rename to packages/mobile-kit/src/screens/Home.tsx diff --git a/apps/mobile/src/screens/LinkedDevices.tsx b/packages/mobile-kit/src/screens/LinkedDevices.tsx similarity index 100% rename from apps/mobile/src/screens/LinkedDevices.tsx rename to packages/mobile-kit/src/screens/LinkedDevices.tsx diff --git a/apps/mobile/src/screens/Notifications.tsx b/packages/mobile-kit/src/screens/Notifications.tsx similarity index 100% rename from apps/mobile/src/screens/Notifications.tsx rename to packages/mobile-kit/src/screens/Notifications.tsx diff --git a/apps/mobile/src/screens/Onboarding.tsx b/packages/mobile-kit/src/screens/Onboarding.tsx similarity index 98% rename from apps/mobile/src/screens/Onboarding.tsx rename to packages/mobile-kit/src/screens/Onboarding.tsx index 8ea39cc..41effc2 100644 --- a/apps/mobile/src/screens/Onboarding.tsx +++ b/packages/mobile-kit/src/screens/Onboarding.tsx @@ -11,6 +11,7 @@ import { Button, ErrorNote, Field, Screen, T } from "../ui/kit"; import { IconFingerprint, IconShield } from "../ui/icons"; import { APP } from "../env"; import { useMobileT, type MobileMessageId } from "../i18n"; +import { brandIcon } from "../brand"; type Step = | { s: "welcome" } @@ -21,7 +22,6 @@ type Step = | { s: "biometrics"; password: string } | { s: "done" }; -const ICON = require("../../assets/icon.png"); /** passwordStrength's English label → [label id, hint id]. */ const STRENGTH: Record = { @@ -73,7 +73,7 @@ function Welcome(props: { onCreate: () => void; onImport: () => void }) { } > - + {brandIcon() ? : null} {APP.config.name} {t("m.onboarding.welcome.lede")} @@ -409,7 +409,7 @@ export function Unlock(props: { onUnlocked: () => void }) { } > - + {brandIcon() ? : null} {t("m.onboarding.unlock.title")} pw && run(() => client.unlock(pw))} /> diff --git a/apps/mobile/src/screens/Plugins.tsx b/packages/mobile-kit/src/screens/Plugins.tsx similarity index 100% rename from apps/mobile/src/screens/Plugins.tsx rename to packages/mobile-kit/src/screens/Plugins.tsx diff --git a/apps/mobile/src/screens/Receive.tsx b/packages/mobile-kit/src/screens/Receive.tsx similarity index 100% rename from apps/mobile/src/screens/Receive.tsx rename to packages/mobile-kit/src/screens/Receive.tsx diff --git a/apps/mobile/src/screens/Scan.tsx b/packages/mobile-kit/src/screens/Scan.tsx similarity index 100% rename from apps/mobile/src/screens/Scan.tsx rename to packages/mobile-kit/src/screens/Scan.tsx diff --git a/apps/mobile/src/screens/Security.tsx b/packages/mobile-kit/src/screens/Security.tsx similarity index 100% rename from apps/mobile/src/screens/Security.tsx rename to packages/mobile-kit/src/screens/Security.tsx diff --git a/apps/mobile/src/screens/Send.tsx b/packages/mobile-kit/src/screens/Send.tsx similarity index 100% rename from apps/mobile/src/screens/Send.tsx rename to packages/mobile-kit/src/screens/Send.tsx diff --git a/apps/mobile/src/screens/Settings.tsx b/packages/mobile-kit/src/screens/Settings.tsx similarity index 98% rename from apps/mobile/src/screens/Settings.tsx rename to packages/mobile-kit/src/screens/Settings.tsx index 3216913..5e32bb7 100644 --- a/apps/mobile/src/screens/Settings.tsx +++ b/packages/mobile-kit/src/screens/Settings.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { Pressable, View } from "react-native"; import { isWalletConnectUri, relativeTime, userMessageOf, type Prefs } from "@clip-wallet/ui"; -import { LOCALES, localeInfo, resolveLocale, type LocalePref } from "@clip-wallet/i18n"; +import { localeInfo, offeredLocales, resolveLocale, type LocalePref } from "@clip-wallet/i18n"; import { useAsync, useWallet } from "../ui/context"; import { Button, Card, ErrorNote, Field, MenuItem, Notice, Row, Screen, T, Toggle } from "../ui/kit"; import { APP } from "../env"; @@ -42,10 +42,10 @@ function Segmented(props: { options: { value: T; labe function LanguagePicker(props: { value: LocalePref; onChange: (v: LocalePref) => void }) { const { theme } = useWallet(); const t = useMobileT(); - const device = localeInfo(resolveLocale("system", deviceLanguages())); + const device = localeInfo(resolveLocale("system", deviceLanguages(), APP.config.languages)); const options: { value: LocalePref; label: string }[] = [ { value: "system", label: t("m.settings.language.system", { language: device.nativeName }) }, - ...LOCALES.map((l) => ({ value: l.code, label: l.nativeName })), + ...offeredLocales(APP.config.languages).map((l) => ({ value: l.code, label: l.nativeName })), ]; return ( diff --git a/apps/mobile/src/screens/SettleFunding.tsx b/packages/mobile-kit/src/screens/SettleFunding.tsx similarity index 100% rename from apps/mobile/src/screens/SettleFunding.tsx rename to packages/mobile-kit/src/screens/SettleFunding.tsx diff --git a/apps/mobile/src/screens/Stake.tsx b/packages/mobile-kit/src/screens/Stake.tsx similarity index 100% rename from apps/mobile/src/screens/Stake.tsx rename to packages/mobile-kit/src/screens/Stake.tsx diff --git a/apps/mobile/src/screens/Swap.tsx b/packages/mobile-kit/src/screens/Swap.tsx similarity index 100% rename from apps/mobile/src/screens/Swap.tsx rename to packages/mobile-kit/src/screens/Swap.tsx diff --git a/apps/mobile/src/screens/Trade.tsx b/packages/mobile-kit/src/screens/Trade.tsx similarity index 100% rename from apps/mobile/src/screens/Trade.tsx rename to packages/mobile-kit/src/screens/Trade.tsx diff --git a/apps/mobile/src/shims/no-pbkdf2.js b/packages/mobile-kit/src/shims/no-pbkdf2.js similarity index 100% rename from apps/mobile/src/shims/no-pbkdf2.js rename to packages/mobile-kit/src/shims/no-pbkdf2.js diff --git a/apps/mobile/src/shims/node-crypto.js b/packages/mobile-kit/src/shims/node-crypto.js similarity index 100% rename from apps/mobile/src/shims/node-crypto.js rename to packages/mobile-kit/src/shims/node-crypto.js diff --git a/apps/mobile/src/ui/context.tsx b/packages/mobile-kit/src/ui/context.tsx similarity index 99% rename from apps/mobile/src/ui/context.tsx rename to packages/mobile-kit/src/ui/context.tsx index 50575c3..2772c3c 100644 --- a/apps/mobile/src/ui/context.tsx +++ b/packages/mobile-kit/src/ui/context.tsx @@ -98,7 +98,7 @@ export function WalletProvider(props: { wallet: MobileWallet; children: ReactNod const mode = state?.prefs.theme && state.prefs.theme !== "system" ? state.prefs.theme : scheme === "dark" ? "dark" : "light"; const theme = useMemo(() => themeFor(APP.config, mode), [mode]); - const locale: LocaleCode = resolveLocale(state?.prefs.locale, deviceLanguages()); + const locale: LocaleCode = resolveLocale(state?.prefs.locale, deviceLanguages(), APP.config.languages); const dir = dirOf(locale); setFormatLocale(locale); useEffect(() => { diff --git a/apps/mobile/src/ui/icons.tsx b/packages/mobile-kit/src/ui/icons.tsx similarity index 100% rename from apps/mobile/src/ui/icons.tsx rename to packages/mobile-kit/src/ui/icons.tsx diff --git a/apps/mobile/src/ui/kit.tsx b/packages/mobile-kit/src/ui/kit.tsx similarity index 100% rename from apps/mobile/src/ui/kit.tsx rename to packages/mobile-kit/src/ui/kit.tsx diff --git a/apps/mobile/src/ui/notifications.ts b/packages/mobile-kit/src/ui/notifications.ts similarity index 100% rename from apps/mobile/src/ui/notifications.ts rename to packages/mobile-kit/src/ui/notifications.ts diff --git a/apps/mobile/src/ui/theme.ts b/packages/mobile-kit/src/ui/theme.ts similarity index 100% rename from apps/mobile/src/ui/theme.ts rename to packages/mobile-kit/src/ui/theme.ts diff --git a/apps/mobile/src/ui/ur.tsx b/packages/mobile-kit/src/ui/ur.tsx similarity index 100% rename from apps/mobile/src/ui/ur.tsx rename to packages/mobile-kit/src/ui/ur.tsx diff --git a/apps/mobile/test/argon2.vitest.ts b/packages/mobile-kit/test/argon2.vitest.ts similarity index 100% rename from apps/mobile/test/argon2.vitest.ts rename to packages/mobile-kit/test/argon2.vitest.ts diff --git a/apps/mobile/test/bridge.vitest.ts b/packages/mobile-kit/test/bridge.vitest.ts similarity index 99% rename from apps/mobile/test/bridge.vitest.ts rename to packages/mobile-kit/test/bridge.vitest.ts index f62dd6d..bb097ed 100644 --- a/apps/mobile/test/bridge.vitest.ts +++ b/packages/mobile-kit/test/bridge.vitest.ts @@ -13,7 +13,7 @@ import { Window as HappyWindow } from "happy-dom"; import { WalletEngine, MemoryKV } from "@clip-wallet/engine"; import { INPAGE_JS } from "../src/browser/inpage.generated"; import { createWebViewBridge, webOrigin } from "../src/browser/bridge"; -import { BASE_SEPOLIA, EVM_ADDRESS, FakeVault, SEPOLIA, makeDeps, makeEnv } from "../../../packages/engine/test/fixtures"; +import { BASE_SEPOLIA, EVM_ADDRESS, FakeVault, SEPOLIA, makeDeps, makeEnv } from "../../engine/test/fixtures"; const ICON = "data:image/svg+xml;base64,PHN2Zy8+"; const mobileDir = join(dirname(fileURLToPath(import.meta.url)), ".."); diff --git a/packages/mobile-kit/test/clip.config.ts b/packages/mobile-kit/test/clip.config.ts new file mode 100644 index 0000000..d95a613 --- /dev/null +++ b/packages/mobile-kit/test/clip.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from "@clip-wallet/config"; + +/** + * Testnet-only service deployments (docs/phase25/deploy.md). A mainnet build gets none of these by default: + * point it at production deployments explicitly. + */ +const MAINNET = false; +const TESTNET_SERVICES = { + backupUrl: "https://clip-backup.doyoka-platform.workers.dev", + mediaProxyUrl: "https://clip-media-proxy.doyoka-platform.workers.dev", + linkRelayUrl: "https://clip-link-relay.doyoka-platform.workers.dev", +}; + +// The config the kit's tests run with (jest and vitest alias virtual:clip-wallet/config here): Clip Wallet's own. + +export default defineConfig({ + name: "Clip Wallet", + rdns: "org.coldai.clipwallet", + icon: "./icon.svg", + theme: { accent: "#FF3C00", accentText: "#FFFFFF", font: "Inter", radius: 14 }, + // All 14 families, testnets only (same as the extension). + networks: ["evm:*", "hedera", "solana", "bitcoin", "sui", "aptos", "cardano", "substrate", "starknet", "ton", "near", "stellar", "tezos", "algorand"], + passkeys: { enabled: true }, + walletConnect: {}, + services: MAINNET ? {} : TESTNET_SERVICES, + // Settle on Hedera (bonded Connectors, @clip-wallet/route SETTLE_DEPLOYMENTS): testnet builds only. + route: { settleOnHedera: !MAINNET }, + mainnet: MAINNET, +}); +export const icon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciLz4="; diff --git a/apps/mobile/test/feature-fixtures.ts b/packages/mobile-kit/test/feature-fixtures.ts similarity index 100% rename from apps/mobile/test/feature-fixtures.ts rename to packages/mobile-kit/test/feature-fixtures.ts diff --git a/apps/mobile/test/features.test.tsx b/packages/mobile-kit/test/features.test.tsx similarity index 100% rename from apps/mobile/test/features.test.tsx rename to packages/mobile-kit/test/features.test.tsx diff --git a/apps/mobile/test/hardware.test.tsx b/packages/mobile-kit/test/hardware.test.tsx similarity index 97% rename from apps/mobile/test/hardware.test.tsx rename to packages/mobile-kit/test/hardware.test.tsx index ebbc37e..ea4f2c7 100644 --- a/apps/mobile/test/hardware.test.tsx +++ b/packages/mobile-kit/test/hardware.test.tsx @@ -7,8 +7,8 @@ import { act, fireEvent, render, screen } from "@testing-library/react-native"; import { AnimatedUr, urFromJson } from "@clip-wallet/hardware/qr"; import { App } from "../src/App"; import { ConnectHardware, HardwareSettings } from "../src/screens/Hardware"; -import { EVM_ADDRESS, fakePort } from "../../../packages/engine/test/fixtures"; -import { HW_ADDRESS } from "../../../packages/engine/test/hardware-fixtures"; +import { EVM_ADDRESS, fakePort } from "../../engine/test/fixtures"; +import { HW_ADDRESS } from "../../engine/test/hardware-fixtures"; import { eventually, renderWith, settle, testWallet } from "./helpers"; const PW = "a long test password 42!"; diff --git a/apps/mobile/test/helpers.tsx b/packages/mobile-kit/test/helpers.tsx similarity index 98% rename from apps/mobile/test/helpers.tsx rename to packages/mobile-kit/test/helpers.tsx index c6a83ba..2d6cecd 100644 --- a/apps/mobile/test/helpers.tsx +++ b/packages/mobile-kit/test/helpers.tsx @@ -12,8 +12,8 @@ import type { MobileWallet } from "../src/background/host"; import { createLinkClient } from "@clip-wallet/ui"; import { Events } from "../src/background/events"; import { WalletProvider, type Route } from "../src/ui/context"; -import { BASE_SEPOLIA, FakeVault, SEPOLIA, makeDeps, makeEnv } from "../../../packages/engine/test/fixtures"; -import { fakeHardwareDeps } from "../../../packages/engine/test/hardware-fixtures"; +import { BASE_SEPOLIA, FakeVault, SEPOLIA, makeDeps, makeEnv } from "../../engine/test/fixtures"; +import { fakeHardwareDeps } from "../../engine/test/hardware-fixtures"; import { FEATURE_ANSWERS, QUEUE } from "./feature-fixtures"; import { SECURITY_ANSWERS } from "./security-fixtures"; diff --git a/apps/mobile/test/i18n-qa.vitest.ts b/packages/mobile-kit/test/i18n-qa.vitest.ts similarity index 100% rename from apps/mobile/test/i18n-qa.vitest.ts rename to packages/mobile-kit/test/i18n-qa.vitest.ts diff --git a/apps/mobile/test/i18n.vitest.ts b/packages/mobile-kit/test/i18n.vitest.ts similarity index 100% rename from apps/mobile/test/i18n.vitest.ts rename to packages/mobile-kit/test/i18n.vitest.ts diff --git a/apps/mobile/test/jest.setup.js b/packages/mobile-kit/test/jest.setup.cjs similarity index 100% rename from apps/mobile/test/jest.setup.js rename to packages/mobile-kit/test/jest.setup.cjs diff --git a/apps/mobile/test/link.test.tsx b/packages/mobile-kit/test/link.test.tsx similarity index 100% rename from apps/mobile/test/link.test.tsx rename to packages/mobile-kit/test/link.test.tsx diff --git a/apps/mobile/test/link.vitest.ts b/packages/mobile-kit/test/link.vitest.ts similarity index 100% rename from apps/mobile/test/link.vitest.ts rename to packages/mobile-kit/test/link.vitest.ts diff --git a/apps/mobile/test/links.vitest.ts b/packages/mobile-kit/test/links.vitest.ts similarity index 100% rename from apps/mobile/test/links.vitest.ts rename to packages/mobile-kit/test/links.vitest.ts diff --git a/apps/mobile/test/notice-routes.vitest.ts b/packages/mobile-kit/test/notice-routes.vitest.ts similarity index 100% rename from apps/mobile/test/notice-routes.vitest.ts rename to packages/mobile-kit/test/notice-routes.vitest.ts diff --git a/apps/mobile/test/platform.test.tsx b/packages/mobile-kit/test/platform.test.tsx similarity index 100% rename from apps/mobile/test/platform.test.tsx rename to packages/mobile-kit/test/platform.test.tsx diff --git a/apps/mobile/test/plugin-fixtures.ts b/packages/mobile-kit/test/plugin-fixtures.ts similarity index 100% rename from apps/mobile/test/plugin-fixtures.ts rename to packages/mobile-kit/test/plugin-fixtures.ts diff --git a/apps/mobile/test/plugins-bridge.vitest.ts b/packages/mobile-kit/test/plugins-bridge.vitest.ts similarity index 100% rename from apps/mobile/test/plugins-bridge.vitest.ts rename to packages/mobile-kit/test/plugins-bridge.vitest.ts diff --git a/apps/mobile/test/plugins-sandbox.vitest.ts b/packages/mobile-kit/test/plugins-sandbox.vitest.ts similarity index 100% rename from apps/mobile/test/plugins-sandbox.vitest.ts rename to packages/mobile-kit/test/plugins-sandbox.vitest.ts diff --git a/apps/mobile/test/screens.test.tsx b/packages/mobile-kit/test/screens.test.tsx similarity index 99% rename from apps/mobile/test/screens.test.tsx rename to packages/mobile-kit/test/screens.test.tsx index cc9feb5..c67ea00 100644 --- a/apps/mobile/test/screens.test.tsx +++ b/packages/mobile-kit/test/screens.test.tsx @@ -5,7 +5,7 @@ import { Send } from "../src/screens/Send"; import { Explore } from "../src/screens/Explore"; import { ApprovalScreen } from "../src/screens/Approval"; import { renderWith, testWallet, WORDS } from "./helpers"; -import { EVM_ADDRESS, fakePort } from "../../../packages/engine/test/fixtures"; +import { EVM_ADDRESS, fakePort } from "../../engine/test/fixtures"; const PW = "a long test password 42!"; diff --git a/apps/mobile/test/security-fixtures.ts b/packages/mobile-kit/test/security-fixtures.ts similarity index 100% rename from apps/mobile/test/security-fixtures.ts rename to packages/mobile-kit/test/security-fixtures.ts diff --git a/apps/mobile/test/security-plugins.test.tsx b/packages/mobile-kit/test/security-plugins.test.tsx similarity index 100% rename from apps/mobile/test/security-plugins.test.tsx rename to packages/mobile-kit/test/security-plugins.test.tsx diff --git a/apps/mobile/test/swap-links.vitest.ts b/packages/mobile-kit/test/swap-links.vitest.ts similarity index 100% rename from apps/mobile/test/swap-links.vitest.ts rename to packages/mobile-kit/test/swap-links.vitest.ts diff --git a/packages/mobile-kit/tsconfig.json b/packages/mobile-kit/tsconfig.json new file mode 100644 index 0000000..1881e61 --- /dev/null +++ b/packages/mobile-kit/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "strict": true, + "noUncheckedIndexedAccess": true, + "verbatimModuleSyntax": false, + // Type-check workspace packages against their TypeScript source ("development"), not the react-native + // condition. Metro still bundles the "react-native" export condition at runtime (see + // docs/phase2/integration/mobile.md, Hedera note). + "customConditions": ["development"], + "types": ["jest", "node"] + }, + "include": ["src", "test", "scripts/*.d.ts"], + "exclude": ["node_modules", "dist", "test/**/*.vitest.ts"] +} diff --git a/packages/mobile-kit/vitest.config.ts b/packages/mobile-kit/vitest.config.ts new file mode 100644 index 0000000..b7ada54 --- /dev/null +++ b/packages/mobile-kit/vitest.config.ts @@ -0,0 +1,9 @@ +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vitest/config"; + +// Node-side tests (WebView bridge end-to-end against the real injected bundle, KDF adapter). Screens use jest-expo. +export default defineConfig({ + // The kit reads the wallet's config from a virtual module that withClipWallet() (metro.cjs) provides in a real build. + resolve: { alias: { "virtual:clip-wallet/config": fileURLToPath(new URL("./test/clip.config.ts", import.meta.url)) } }, + test: { include: ["test/**/*.vitest.ts"], environment: "node" }, +}); diff --git a/packages/ui/src/context.tsx b/packages/ui/src/context.tsx index be0818e..64fed0a 100644 --- a/packages/ui/src/context.tsx +++ b/packages/ui/src/context.tsx @@ -79,7 +79,7 @@ export function ClipProvider(props: { return () => mq.removeEventListener?.("change", on); }, []); - const locale: LocaleCode = resolveLocale(state?.prefs.locale, deviceLanguages()); + const locale: LocaleCode = resolveLocale(state?.prefs.locale, deviceLanguages(), config.languages); setFormatLocale(locale); useEffect(() => { document.documentElement.lang = locale; diff --git a/packages/ui/src/screens/Settings.tsx b/packages/ui/src/screens/Settings.tsx index 831cdb5..fc7eb69 100644 --- a/packages/ui/src/screens/Settings.tsx +++ b/packages/ui/src/screens/Settings.tsx @@ -12,7 +12,7 @@ import { useSecurityOptional } from "../security/context"; import { useLinkOptional } from "../link/context"; import { useSocialOptional } from "../social/context"; import { useUiT } from "../i18n"; -import { LOCALES, localeInfo, resolveLocale, type LocalePref } from "@clip-wallet/i18n"; +import { localeInfo, offeredLocales, resolveLocale, type LocalePref } from "@clip-wallet/i18n"; const AUTO_LOCK = [1, 5, 15, 30, 60]; @@ -210,8 +210,8 @@ export function Settings() { value={prefs.locale ?? "system"} onChange={(e) => setPrefs({ locale: e.target.value as LocalePref })} > - - {LOCALES.map((l) => ( + + {offeredLocales(config.languages).map((l) => ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6e7da6..4492191 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -190,57 +190,21 @@ importers: apps/desktop: devDependencies: - '@clip-wallet/1mask': - specifier: workspace:* - version: link:../../packages/1mask - '@clip-wallet/chains-ton': - specifier: workspace:* - version: link:../../packages/chains-ton '@clip-wallet/config': specifier: workspace:* version: link:../../packages/config - '@clip-wallet/core': + '@clip-wallet/desktop-kit': specifier: workspace:* - version: link:../../packages/core + version: link:../../packages/desktop-kit '@clip-wallet/engine': specifier: workspace:* version: link:../../packages/engine - '@clip-wallet/features': - specifier: workspace:* - version: link:../../packages/features - '@clip-wallet/hardware': - specifier: workspace:* - version: link:../../packages/hardware - '@clip-wallet/i18n': - specifier: workspace:* - version: link:../../packages/i18n '@clip-wallet/link': specifier: workspace:* version: link:../../packages/link - '@clip-wallet/security': - specifier: workspace:* - version: link:../../packages/security - '@clip-wallet/social': - specifier: workspace:* - version: link:../../packages/social - '@clip-wallet/ui': - specifier: workspace:* - version: link:../../packages/ui '@clip-wallet/vault': specifier: workspace:* version: link:../../packages/vault - '@fontsource-variable/inter': - specifier: ^5.3.0 - version: 5.3.0 - '@ledgerhq/hw-transport': - specifier: 6.36.0 - version: 6.36.0 - '@ledgerhq/hw-transport-webhid': - specifier: 6.36.0 - version: 6.36.0 - '@noble/hashes': - specifier: 2.4.0 - version: 2.4.0 '@playwright/test': specifier: ^1.63.0 version: 1.63.0 @@ -253,27 +217,15 @@ importers: '@types/react-dom': specifier: ^19.3.0 version: 19.3.0(@types/react@19.3.0) - '@vitejs/plugin-react': - specifier: ^5.1.0 - version: 5.2.0(supports-color@8.1.1)(vite@7.3.6(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.51.2)(yaml@2.9.1)) - buffer: - specifier: ^6.0.3 - version: 6.0.3 electron: specifier: 44.5.1 version: 44.5.1(supports-color@8.1.1) electron-builder: specifier: ^26.15.3 version: 26.15.3(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1) - electron-updater: - specifier: ^6.8.9 - version: 6.8.9(supports-color@8.1.1) electron-vite: specifier: ^5.0.0 version: 5.0.0(supports-color@8.1.1)(vite@7.3.6(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.51.2)(yaml@2.9.1)) - esbuild: - specifier: ^0.25.0 - version: 0.25.12 react: specifier: ^19.3.0 version: 19.3.0 @@ -289,12 +241,6 @@ importers: vite: specifier: ^7.1.0 version: 7.3.6(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.51.2)(yaml@2.9.1) - vitest: - specifier: ~4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.6(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.51.2)(yaml@2.9.1)) - zod: - specifier: ^4.6.5 - version: 4.6.5 apps/docs: devDependencies: @@ -909,54 +855,15 @@ importers: apps/mobile: dependencies: - '@clip-wallet/1mask': - specifier: workspace:* - version: link:../../packages/1mask '@clip-wallet/config': specifier: workspace:* version: link:../../packages/config - '@clip-wallet/core': - specifier: workspace:* - version: link:../../packages/core - '@clip-wallet/engine': - specifier: workspace:* - version: link:../../packages/engine - '@clip-wallet/features': - specifier: workspace:* - version: link:../../packages/features - '@clip-wallet/hardware': - specifier: workspace:* - version: link:../../packages/hardware - '@clip-wallet/i18n': - specifier: workspace:* - version: link:../../packages/i18n - '@clip-wallet/link': - specifier: workspace:* - version: link:../../packages/link - '@clip-wallet/names': + '@clip-wallet/mobile-kit': specifier: workspace:* - version: link:../../packages/names - '@clip-wallet/plugins': - specifier: workspace:* - version: link:../../packages/plugins - '@clip-wallet/security': - specifier: workspace:* - version: link:../../packages/security - '@clip-wallet/social': - specifier: workspace:* - version: link:../../packages/social - '@clip-wallet/ui': - specifier: workspace:* - version: link:../../packages/ui - '@clip-wallet/vault': - specifier: workspace:* - version: link:../../packages/vault + version: link:../../packages/mobile-kit '@ledgerhq/react-native-hw-transport-ble': specifier: 6.41.0 version: 6.41.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3) - '@noble/hashes': - specifier: 2.4.0 - version: 2.4.0 '@react-native-async-storage/async-storage': specifier: 2.2.0 version: 2.2.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10)) @@ -1017,12 +924,6 @@ importers: expo-web-browser: specifier: ~57.0.3 version: 57.0.3(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10)) - pako: - specifier: 2.2.0 - version: 2.2.0 - qrcode: - specifier: ^1.5.4 - version: 1.5.4 react: specifier: 19.2.3 version: 19.2.3 @@ -1050,49 +951,16 @@ importers: react-native-webview: specifier: 13.16.1 version: 13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3) - readable-stream: - specifier: ^3.6.2 - version: 3.6.2 - ses: - specifier: 2.3.0 - version: 2.3.0 devDependencies: - '@react-native/jest-preset': - specifier: 0.86.3 - version: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) - '@testing-library/react-native': - specifier: ^13.3.3 - version: 13.3.3(jest@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@8.1.1))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react-test-renderer@19.2.3(react@19.2.3))(react@19.2.3) - '@types/jest': - specifier: ^29.5.14 - version: 29.5.14 - '@types/qrcode': - specifier: ^1.5.6 - version: 1.5.6 + '@types/node': + specifier: ^22.20.5 + version: 22.20.5 '@types/react': specifier: ~19.2.0 version: 19.2.18 - esbuild: - specifier: ^0.25.0 - version: 0.25.12 - happy-dom: - specifier: ^20.14.5 - version: 20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@8.1.1) - jest-expo: - specifier: ~57.0.5 - version: 57.0.5(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(bufferutil@4.1.0)(expo@57.0.26)(jest@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@8.1.1))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10) - react-test-renderer: - specifier: 19.2.3 - version: 19.2.3(react@19.2.3) typescript: specifier: ^5.9.3 version: 5.9.3 - vitest: - specifier: ~4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) packages/1mask: dependencies: @@ -1516,53 +1384,11 @@ importers: specifier: ~4.1.11 version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) - packages/engine: + packages/desktop-kit: dependencies: '@clip-wallet/1mask': specifier: workspace:* version: link:../1mask - '@clip-wallet/backup-client': - specifier: workspace:* - version: link:../backup-client - '@clip-wallet/chains-algorand': - specifier: workspace:* - version: link:../chains-algorand - '@clip-wallet/chains-aptos': - specifier: workspace:* - version: link:../chains-aptos - '@clip-wallet/chains-bitcoin': - specifier: workspace:* - version: link:../chains-bitcoin - '@clip-wallet/chains-cardano': - specifier: workspace:* - version: link:../chains-cardano - '@clip-wallet/chains-evm': - specifier: workspace:* - version: link:../chains-evm - '@clip-wallet/chains-hedera': - specifier: workspace:* - version: link:../chains-hedera - '@clip-wallet/chains-near': - specifier: workspace:* - version: link:../chains-near - '@clip-wallet/chains-solana': - specifier: workspace:* - version: link:../chains-solana - '@clip-wallet/chains-starknet': - specifier: workspace:* - version: link:../chains-starknet - '@clip-wallet/chains-stellar': - specifier: workspace:* - version: link:../chains-stellar - '@clip-wallet/chains-substrate': - specifier: workspace:* - version: link:../chains-substrate - '@clip-wallet/chains-sui': - specifier: workspace:* - version: link:../chains-sui - '@clip-wallet/chains-tezos': - specifier: workspace:* - version: link:../chains-tezos '@clip-wallet/chains-ton': specifier: workspace:* version: link:../chains-ton @@ -1572,6 +1398,9 @@ importers: '@clip-wallet/core': specifier: workspace:* version: link:../core + '@clip-wallet/engine': + specifier: workspace:* + version: link:../engine '@clip-wallet/features': specifier: workspace:* version: link:../features @@ -1581,15 +1410,9 @@ importers: '@clip-wallet/i18n': specifier: workspace:* version: link:../i18n - '@clip-wallet/names': - specifier: workspace:* - version: link:../names - '@clip-wallet/plugins': - specifier: workspace:* - version: link:../plugins - '@clip-wallet/route': + '@clip-wallet/link': specifier: workspace:* - version: link:../route + version: link:../link '@clip-wallet/security': specifier: workspace:* version: link:../security @@ -1599,21 +1422,69 @@ importers: '@clip-wallet/ui': specifier: workspace:* version: link:../ui + '@clip-wallet/vault': + specifier: workspace:* + version: link:../vault + '@fontsource-variable/inter': + specifier: ^5.3.0 + version: 5.3.0 + '@ledgerhq/hw-transport': + specifier: 6.36.0 + version: 6.36.0 + '@ledgerhq/hw-transport-webhid': + specifier: 6.36.0 + version: 6.36.0 '@noble/hashes': specifier: 2.4.0 version: 2.4.0 + '@vitejs/plugin-react': + specifier: ^5.1.0 + version: 5.2.0(supports-color@10.2.2)(vite@7.3.6(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.51.2)(yaml@2.9.1)) + buffer: + specifier: ^6.0.3 + version: 6.0.3 + electron-updater: + specifier: ^6.8.9 + version: 6.8.9(supports-color@10.2.2) + esbuild: + specifier: ^0.25.0 + version: 0.25.12 zod: specifier: ^4.6.5 version: 4.6.5 devDependencies: + '@types/node': + specifier: ^22.20.5 + version: 22.20.5 + '@types/react': + specifier: ^19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.3.0 + version: 19.3.0(@types/react@19.3.0) + electron: + specifier: 44.5.1 + version: 44.5.1(supports-color@10.2.2) + electron-vite: + specifier: ^5.0.0 + version: 5.0.0(supports-color@10.2.2)(vite@7.3.6(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.51.2)(yaml@2.9.1)) + react: + specifier: ^19.3.0 + version: 19.3.0 + react-dom: + specifier: ^19.3.0 + version: 19.3.0(react@19.3.0) typescript: specifier: ^5.9.3 version: 5.9.3 + vite: + specifier: ^7.1.0 + version: 7.3.6(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.51.2)(yaml@2.9.1) vitest: specifier: ~4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.6(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.51.2)(yaml@2.9.1)) - packages/extension-kit: + packages/engine: dependencies: '@clip-wallet/1mask': specifier: workspace:* @@ -1669,9 +1540,6 @@ importers: '@clip-wallet/core': specifier: workspace:* version: link:../core - '@clip-wallet/engine': - specifier: workspace:* - version: link:../engine '@clip-wallet/features': specifier: workspace:* version: link:../features @@ -1681,12 +1549,6 @@ importers: '@clip-wallet/i18n': specifier: workspace:* version: link:../i18n - '@clip-wallet/kit-modules': - specifier: workspace:* - version: link:../kit-modules - '@clip-wallet/link': - specifier: workspace:* - version: link:../link '@clip-wallet/names': specifier: workspace:* version: link:../names @@ -1705,51 +1567,157 @@ importers: '@clip-wallet/ui': specifier: workspace:* version: link:../ui - '@clip-wallet/vault': - specifier: workspace:* - version: link:../vault - '@fontsource-variable/inter': - specifier: ^5.3.0 - version: 5.3.0 - buffer: - specifier: ^6.0.3 - version: 6.0.3 - viem: - specifier: ^2.57.1 - version: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.6.5) + '@noble/hashes': + specifier: 2.4.0 + version: 2.4.0 zod: specifier: ^4.6.5 version: 4.6.5 devDependencies: - '@types/node': - specifier: ^22.0.0 - version: 22.20.5 - '@types/react': - specifier: ^19.3.0 - version: 19.3.0 - '@types/react-dom': - specifier: ^19.3.0 - version: 19.3.0(@types/react@19.3.0) - '@wxt-dev/module-react': - specifier: ^1.2.2 - version: 1.2.2(babel-plugin-react-compiler@1.0.0)(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))(wxt@0.21.4(esbuild@0.28.1)(rolldown@1.2.12)(rollup@4.63.6)(typescript@5.9.3)(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) - react: - specifier: ^19.3.0 - version: 19.3.0 - react-dom: - specifier: ^19.3.0 - version: 19.3.0(react@19.3.0) typescript: specifier: ^5.9.3 version: 5.9.3 vitest: specifier: ~4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) - wxt: - specifier: ^0.21.4 - version: 0.21.4(esbuild@0.28.1)(rolldown@1.2.12)(rollup@4.63.6)(typescript@5.9.3)(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) - packages/features: + packages/extension-kit: + dependencies: + '@clip-wallet/1mask': + specifier: workspace:* + version: link:../1mask + '@clip-wallet/backup-client': + specifier: workspace:* + version: link:../backup-client + '@clip-wallet/chains-algorand': + specifier: workspace:* + version: link:../chains-algorand + '@clip-wallet/chains-aptos': + specifier: workspace:* + version: link:../chains-aptos + '@clip-wallet/chains-bitcoin': + specifier: workspace:* + version: link:../chains-bitcoin + '@clip-wallet/chains-cardano': + specifier: workspace:* + version: link:../chains-cardano + '@clip-wallet/chains-evm': + specifier: workspace:* + version: link:../chains-evm + '@clip-wallet/chains-hedera': + specifier: workspace:* + version: link:../chains-hedera + '@clip-wallet/chains-near': + specifier: workspace:* + version: link:../chains-near + '@clip-wallet/chains-solana': + specifier: workspace:* + version: link:../chains-solana + '@clip-wallet/chains-starknet': + specifier: workspace:* + version: link:../chains-starknet + '@clip-wallet/chains-stellar': + specifier: workspace:* + version: link:../chains-stellar + '@clip-wallet/chains-substrate': + specifier: workspace:* + version: link:../chains-substrate + '@clip-wallet/chains-sui': + specifier: workspace:* + version: link:../chains-sui + '@clip-wallet/chains-tezos': + specifier: workspace:* + version: link:../chains-tezos + '@clip-wallet/chains-ton': + specifier: workspace:* + version: link:../chains-ton + '@clip-wallet/config': + specifier: workspace:* + version: link:../config + '@clip-wallet/core': + specifier: workspace:* + version: link:../core + '@clip-wallet/engine': + specifier: workspace:* + version: link:../engine + '@clip-wallet/features': + specifier: workspace:* + version: link:../features + '@clip-wallet/hardware': + specifier: workspace:* + version: link:../hardware + '@clip-wallet/i18n': + specifier: workspace:* + version: link:../i18n + '@clip-wallet/kit-modules': + specifier: workspace:* + version: link:../kit-modules + '@clip-wallet/link': + specifier: workspace:* + version: link:../link + '@clip-wallet/names': + specifier: workspace:* + version: link:../names + '@clip-wallet/plugins': + specifier: workspace:* + version: link:../plugins + '@clip-wallet/route': + specifier: workspace:* + version: link:../route + '@clip-wallet/security': + specifier: workspace:* + version: link:../security + '@clip-wallet/social': + specifier: workspace:* + version: link:../social + '@clip-wallet/ui': + specifier: workspace:* + version: link:../ui + '@clip-wallet/vault': + specifier: workspace:* + version: link:../vault + '@fontsource-variable/inter': + specifier: ^5.3.0 + version: 5.3.0 + buffer: + specifier: ^6.0.3 + version: 6.0.3 + viem: + specifier: ^2.57.1 + version: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.6.5) + zod: + specifier: ^4.6.5 + version: 4.6.5 + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.20.5 + '@types/react': + specifier: ^19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.3.0 + version: 19.3.0(@types/react@19.3.0) + '@wxt-dev/module-react': + specifier: ^1.2.2 + version: 1.2.2(babel-plugin-react-compiler@1.0.0)(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))(wxt@0.21.4(esbuild@0.28.1)(rolldown@1.2.12)(rollup@4.63.6)(typescript@5.9.3)(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + react: + specifier: ^19.3.0 + version: 19.3.0 + react-dom: + specifier: ^19.3.0 + version: 19.3.0(react@19.3.0) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ~4.1.11 + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) + wxt: + specifier: ^0.21.4 + version: 0.21.4(esbuild@0.28.1)(rolldown@1.2.12)(rollup@4.63.6)(typescript@5.9.3)(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) + + packages/features: dependencies: '@clip-wallet/chains-algorand': specifier: workspace:* @@ -1990,6 +1958,196 @@ importers: specifier: ~4.1.11 version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) + packages/mobile-kit: + dependencies: + '@clip-wallet/1mask': + specifier: workspace:* + version: link:../1mask + '@clip-wallet/config': + specifier: workspace:* + version: link:../config + '@clip-wallet/core': + specifier: workspace:* + version: link:../core + '@clip-wallet/engine': + specifier: workspace:* + version: link:../engine + '@clip-wallet/features': + specifier: workspace:* + version: link:../features + '@clip-wallet/hardware': + specifier: workspace:* + version: link:../hardware + '@clip-wallet/i18n': + specifier: workspace:* + version: link:../i18n + '@clip-wallet/link': + specifier: workspace:* + version: link:../link + '@clip-wallet/names': + specifier: workspace:* + version: link:../names + '@clip-wallet/plugins': + specifier: workspace:* + version: link:../plugins + '@clip-wallet/security': + specifier: workspace:* + version: link:../security + '@clip-wallet/social': + specifier: workspace:* + version: link:../social + '@clip-wallet/ui': + specifier: workspace:* + version: link:../ui + '@clip-wallet/vault': + specifier: workspace:* + version: link:../vault + '@noble/hashes': + specifier: 2.4.0 + version: 2.4.0 + pako: + specifier: 2.2.0 + version: 2.2.0 + qrcode: + specifier: ^1.5.4 + version: 1.5.4 + readable-stream: + specifier: ^3.6.2 + version: 3.6.2 + ses: + specifier: 2.3.0 + version: 2.3.0 + devDependencies: + '@ledgerhq/react-native-hw-transport-ble': + specifier: 6.41.0 + version: 6.41.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + '@react-native-async-storage/async-storage': + specifier: 2.2.0 + version: 2.2.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)) + '@react-native-community/netinfo': + specifier: 12.0.1 + version: 12.0.1(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + '@react-native/jest-preset': + specifier: 0.86.3 + version: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2) + '@testing-library/react-native': + specifier: ^13.3.3 + version: 13.3.3(jest@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@10.2.2))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react-test-renderer@19.2.3(react@19.2.3))(react@19.2.3) + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + '@types/node': + specifier: ^22.20.5 + version: 22.20.5 + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 + '@types/react': + specifier: ~19.2.0 + version: 19.2.18 + '@walletconnect/react-native-compat': + specifier: 2.25.0 + version: 2.25.0(846135d3436568983c2ddd3320193643) + esbuild: + specifier: ^0.25.0 + version: 0.25.12 + expo: + specifier: ~57.0.26 + version: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo-application: + specifier: ~57.0.3 + version: 57.0.3(expo@57.0.26) + expo-background-task: + specifier: ~57.0.21 + version: 57.0.21(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)) + expo-camera: + specifier: ~57.0.6 + version: 57.0.6(@types/emscripten@1.41.6)(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + expo-clipboard: + specifier: ~57.0.2 + version: 57.0.2(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + expo-crypto: + specifier: ~57.0.3 + version: 57.0.3(expo@57.0.26) + expo-dev-client: + specifier: ~57.0.19 + version: 57.0.19(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)) + expo-device: + specifier: ~57.0.2 + version: 57.0.2(expo@57.0.26) + expo-linking: + specifier: ~57.0.11 + version: 57.0.11(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2) + expo-local-authentication: + specifier: ~57.0.3 + version: 57.0.3(expo@57.0.26) + expo-localization: + specifier: ~57.0.2 + version: 57.0.2(expo@57.0.26)(react@19.2.3) + expo-notifications: + specifier: ~57.0.21 + version: 57.0.21(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3) + expo-secure-store: + specifier: ~57.0.4 + version: 57.0.4(expo@57.0.26) + expo-splash-screen: + specifier: ~57.0.9 + version: 57.0.9(expo@57.0.26)(supports-color@10.2.2)(typescript@5.9.3) + expo-status-bar: + specifier: ~57.0.1 + version: 57.0.1(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + expo-task-manager: + specifier: ~57.0.21 + version: 57.0.21(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)) + expo-web-browser: + specifier: ~57.0.3 + version: 57.0.3(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)) + happy-dom: + specifier: ^20.14.5 + version: 20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@10.2.2) + jest-expo: + specifier: ~57.0.5 + version: 57.0.5(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(bufferutil@4.1.0)(expo@57.0.26)(jest@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@10.2.2))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + react: + specifier: 19.2.3 + version: 19.2.3 + react-native: + specifier: 0.86.3 + version: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + react-native-argon2: + specifier: 4.0.0 + version: 4.0.0 + react-native-ble-plx: + specifier: 3.4.0 + version: 3.4.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + react-native-get-random-values: + specifier: ~1.11.0 + version: 1.11.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)) + react-native-passkey: + specifier: 3.6.2 + version: 3.6.2(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + react-native-safe-area-context: + specifier: ~5.7.0 + version: 5.7.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + react-native-svg: + specifier: 15.15.4 + version: 15.15.4(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + react-native-webview: + specifier: 13.16.1 + version: 13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + react-test-renderer: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ~4.1.11 + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) + packages/names: dependencies: '@clip-wallet/core': @@ -19618,7 +19776,6 @@ snapshots: dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 - optional: true '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.7(supports-color@5.5.0))': dependencies: @@ -19635,7 +19792,6 @@ snapshots: dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 - optional: true '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.7(supports-color@5.5.0))': dependencies: @@ -19652,7 +19808,6 @@ snapshots: dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 - optional: true '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.7(supports-color@5.5.0))': dependencies: @@ -19669,7 +19824,6 @@ snapshots: dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 - optional: true '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.7(supports-color@5.5.0))': dependencies: @@ -19750,7 +19904,6 @@ snapshots: dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 - optional: true '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7(supports-color@5.5.0))': dependencies: @@ -19767,7 +19920,6 @@ snapshots: dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 - optional: true '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.7(supports-color@5.5.0))': dependencies: @@ -19784,7 +19936,6 @@ snapshots: dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 - optional: true '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.7(supports-color@5.5.0))': dependencies: @@ -19816,7 +19967,6 @@ snapshots: dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 - optional: true '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.7(supports-color@5.5.0))': dependencies: @@ -19849,7 +19999,6 @@ snapshots: dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 - optional: true '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.7(supports-color@5.5.0))': dependencies: @@ -19866,7 +20015,6 @@ snapshots: dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 - optional: true '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.7(supports-color@5.5.0))': dependencies: @@ -19883,7 +20031,6 @@ snapshots: dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 - optional: true '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.7(supports-color@5.5.0))': dependencies: @@ -19916,7 +20063,6 @@ snapshots: dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 - optional: true '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.7(supports-color@5.5.0))': dependencies: @@ -19933,7 +20079,6 @@ snapshots: dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 - optional: true '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.7(supports-color@5.5.0))': dependencies: @@ -19962,6 +20107,11 @@ snapshots: '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-arrow-functions@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))': + dependencies: + '@babel/core': 7.29.7(supports-color@10.2.2) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-arrow-functions@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: '@babel/core': 7.29.7(supports-color@8.1.1) @@ -20469,14 +20619,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))': dependencies: - '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))': dependencies: - '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-react-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)': @@ -22196,6 +22346,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@electron/get@5.1.0(supports-color@10.2.2)': + dependencies: + debug: 4.4.3(supports-color@10.2.2) + env-paths: 3.0.0 + graceful-fs: 4.2.11 + progress: 2.0.3 + semver: 7.8.5 + sumchecker: 3.0.1(supports-color@10.2.2) + optionalDependencies: + undici: 7.29.0 + transitivePeerDependencies: + - supports-color + '@electron/get@5.1.0(supports-color@8.1.1)': dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -23051,6 +23214,83 @@ snapshots: - typescript - utf-8-validate + '@expo/cli@57.0.27(6121b4fccc0cdac69a0d80123b73f485)': + dependencies: + '@expo/code-signing-certificates': 0.0.6 + '@expo/config': 57.0.9(supports-color@10.2.2)(typescript@5.9.3) + '@expo/config-plugins': 57.0.9(supports-color@10.2.2)(typescript@5.9.3) + '@expo/devcert': 1.2.2(supports-color@10.2.2) + '@expo/env': 2.4.3(supports-color@10.2.2) + '@expo/image-utils': 0.11.5(supports-color@10.2.2)(typescript@5.9.3) + '@expo/inline-modules': 0.1.7(supports-color@10.2.2)(typescript@5.9.3) + '@expo/json-file': 11.0.1 + '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + '@expo/metro': 56.0.2(bufferutil@4.1.0)(supports-color@10.2.2)(utf-8-validate@5.0.10) + '@expo/metro-config': 57.0.12(bufferutil@4.1.0)(expo@57.0.26)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@expo/metro-file-map': 57.0.3(supports-color@10.2.2) + '@expo/osascript': 2.7.1 + '@expo/package-manager': 1.13.1 + '@expo/plist': 0.8.1 + '@expo/prebuild-config': 57.0.16(supports-color@10.2.2)(typescript@5.9.3) + '@expo/require-utils': 57.0.5(supports-color@10.2.2)(typescript@5.9.3) + '@expo/router-server': 57.0.11(expo-constants@57.0.20(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(supports-color@10.2.2))(expo-font@57.0.4(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(expo-server@57.0.3)(expo@57.0.26)(react-dom@19.3.0(react@19.2.3))(react@19.2.3)(supports-color@10.2.2) + '@expo/schema-utils': 57.0.2 + '@expo/spawn-async': 1.8.0 + '@expo/ws-tunnel': 2.0.1(ws@8.22.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@expo/xcpretty': 4.4.5 + '@react-native/dev-middleware': 0.86.3(bufferutil@4.1.0)(supports-color@10.2.2)(utf-8-validate@5.0.10) + accepts: 1.3.8 + agent-cli-detector: 0.1.7 + arg: 5.0.2 + bplist-creator: 0.1.0 + bplist-parser: 0.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + compression: 1.8.2(supports-color@10.2.2) + connect: 3.7.0(supports-color@10.2.2) + debug: 4.4.3(supports-color@10.2.2) + dnssd-advertise: 1.1.6 + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo-server: 57.0.3 + fetch-nodeshim: 0.4.10 + getenv: 2.0.0 + glob: 13.0.6 + lan-network: 0.2.1 + multitars: 1.0.2 + node-forge: 1.4.0 + npm-package-arg: 11.0.3 + ora: 3.4.0 + picomatch: 4.0.7 + pretty-format: 29.7.0 + progress: 2.0.3 + prompts: 2.4.2 + resolve-from: 5.0.0 + sandbox-cli-detector: 0.2.0 + semver: 7.8.5 + send: 0.19.2(supports-color@10.2.2) + slugify: 1.6.9 + stacktrace-parser: 0.1.11 + structured-headers: 0.4.1 + terminal-link: 2.1.1 + toqr: 0.1.1 + wrap-ansi: 7.0.0 + ws: 8.22.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + zod: 3.25.76 + optionalDependencies: + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - '@expo/dom-webview' + - '@expo/metro-runtime' + - bufferutil + - expo-constants + - expo-font + - react + - react-dom + - react-server-dom-webpack + - supports-color + - typescript + - utf-8-validate + '@expo/cli@57.0.27(7069ef8588b79f3b6ed0f31d5eda0bec)': dependencies: '@expo/code-signing-certificates': 0.0.6 @@ -23341,6 +23581,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@expo/devtools@57.0.1(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)': + dependencies: + chalk: 4.1.2 + optionalDependencies: + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + '@expo/devtools@57.0.1(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0)': dependencies: chalk: 4.1.2 @@ -23363,6 +23610,12 @@ snapshots: react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10) + '@expo/dom-webview@57.0.1(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)': + dependencies: + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + '@expo/dom-webview@57.0.1(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0)': dependencies: expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.3.0))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -23550,6 +23803,15 @@ snapshots: - supports-color - typescript + '@expo/log-box@57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)': + dependencies: + '@expo/dom-webview': 57.0.1(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + anser: 1.4.10 + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + stacktrace-parser: 0.1.11 + '@expo/log-box@57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0)': dependencies: '@expo/dom-webview': 57.0.1(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0) @@ -23875,6 +24137,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@expo/router-server@57.0.11(expo-constants@57.0.20(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(supports-color@10.2.2))(expo-font@57.0.4(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(expo-server@57.0.3)(expo@57.0.26)(react-dom@19.3.0(react@19.2.3))(react@19.2.3)(supports-color@10.2.2)': + dependencies: + debug: 4.4.3(supports-color@10.2.2) + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo-constants: 57.0.20(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(supports-color@10.2.2) + expo-font: 57.0.4(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + expo-server: 57.0.3 + react: 19.2.3 + optionalDependencies: + react-dom: 19.3.0(react@19.2.3) + transitivePeerDependencies: + - supports-color + '@expo/router-server@57.0.11(expo-constants@57.0.20(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(supports-color@10.2.2))(expo-font@57.0.4(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0))(expo-server@57.0.3)(expo@57.0.26)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)': dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -24657,6 +24932,41 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(supports-color@10.2.2)': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0(supports-color@10.2.2) + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0(supports-color@10.2.2) + '@jest/types': 29.6.3 + '@types/node': 22.20.5 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@10.2.2) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0(supports-color@10.2.2) + jest-runner: 29.7.0(supports-color@10.2.2) + jest-runtime: 29.7.0(supports-color@10.2.2) + jest-snapshot: 29.7.0(supports-color@10.2.2) + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(supports-color@8.1.1)': dependencies: '@jest/console': 29.7.0 @@ -24709,6 +25019,13 @@ snapshots: dependencies: jest-get-type: 29.6.3 + '@jest/expect@29.7.0(supports-color@10.2.2)': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + '@jest/expect@29.7.0(supports-color@8.1.1)': dependencies: expect: 29.7.0 @@ -24727,6 +25044,15 @@ snapshots: '@jest/get-type@30.5.0': {} + '@jest/globals@29.7.0(supports-color@10.2.2)': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0(supports-color@10.2.2) + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + '@jest/globals@29.7.0(supports-color@8.1.1)': dependencies: '@jest/environment': 29.7.0 @@ -24736,6 +25062,35 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/reporters@29.7.0(supports-color@10.2.2)': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0(supports-color@10.2.2) + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 22.20.5 + chalk: 4.1.2 + collect-v8-coverage: 1.0.3 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3(supports-color@10.2.2) + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1(supports-color@10.2.2) + istanbul-reports: 3.2.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + '@jest/reporters@29.7.0(supports-color@8.1.1)': dependencies: '@bcoe/v8-coverage': 0.2.3 @@ -24812,7 +25167,6 @@ snapshots: write-file-atomic: 4.0.2 transitivePeerDependencies: - supports-color - optional: true '@jest/transform@29.7.0(supports-color@5.5.0)': dependencies: @@ -25148,6 +25502,19 @@ snapshots: '@ledgerhq/logs@6.19.0': {} + '@ledgerhq/react-native-hw-transport-ble@6.41.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)': + dependencies: + '@ledgerhq/devices': 8.16.0 + '@ledgerhq/errors': 6.37.0 + '@ledgerhq/hw-transport': 6.35.5 + '@ledgerhq/logs': 6.19.0 + react-native-ble-plx: 3.4.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + rxjs: 7.8.2 + uuid: 9.0.1 + transitivePeerDependencies: + - react + - react-native + '@ledgerhq/react-native-hw-transport-ble@6.41.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)': dependencies: '@ledgerhq/devices': 8.16.0 @@ -27940,11 +28307,21 @@ snapshots: react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10) optional: true + '@react-native-async-storage/async-storage@2.2.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))': + dependencies: + merge-options: 3.0.4 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + '@react-native-async-storage/async-storage@2.2.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))': dependencies: merge-options: 3.0.4 react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10) + '@react-native-community/netinfo@12.0.1(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)': + dependencies: + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + '@react-native-community/netinfo@12.0.1(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)': dependencies: react: 19.2.3 @@ -28134,6 +28511,18 @@ snapshots: '@react-native/gradle-plugin@0.86.3': {} + '@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2)': + dependencies: + '@jest/create-cache-key-function': 29.7.0 + '@react-native/js-polyfills': 0.86.3 + babel-jest: 29.7.0(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) + jest-environment-node: 29.7.0 + react: 19.2.3 + regenerator-runtime: 0.13.11 + transitivePeerDependencies: + - '@babel/core' + - supports-color + '@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)': dependencies: '@jest/create-cache-key-function': 29.7.0 @@ -28171,6 +28560,7 @@ snapshots: transitivePeerDependencies: - '@babel/core' - supports-color + optional: true '@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.3.0)(supports-color@8.1.1)': dependencies: @@ -28189,6 +28579,15 @@ snapshots: '@react-native/normalize-colors@0.86.3': {} + '@react-native/virtualized-lists@0.86.3(@types/react@19.2.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)': + dependencies: + invariant: 2.2.4 + nullthrows: 1.1.1 + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + optionalDependencies: + '@types/react': 19.2.18 + '@react-native/virtualized-lists@0.86.3(@types/react@19.2.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)': dependencies: invariant: 2.2.4 @@ -34144,17 +34543,17 @@ snapshots: optionalDependencies: vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) - '@testing-library/react-native@13.3.3(jest@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@8.1.1))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react-test-renderer@19.2.3(react@19.2.3))(react@19.2.3)': + '@testing-library/react-native@13.3.3(jest@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@10.2.2))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react-test-renderer@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: jest-matcher-utils: 30.5.2 picocolors: 1.1.1 pretty-format: 30.5.1 react: 19.2.3 - react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) react-test-renderer: 19.2.3(react@19.2.3) redent: 3.0.0 optionalDependencies: - jest: 29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@8.1.1) + jest: 29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@10.2.2) '@testing-library/react@16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': dependencies: @@ -34956,11 +35355,11 @@ snapshots: dependencies: '@vanilla-extract/css': 1.20.1(babel-plugin-macros@3.1.0) - '@vitejs/plugin-react@5.2.0(supports-color@8.1.1)(vite@7.3.6(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.51.2)(yaml@2.9.1))': + '@vitejs/plugin-react@5.2.0(supports-color@10.2.2)(vite@7.3.6(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.51.2)(yaml@2.9.1))': dependencies: - '@babel/core': 7.29.7(supports-color@8.1.1) - '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) - '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/core': 7.29.7(supports-color@10.2.2) + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2)) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2)) '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 @@ -36733,6 +37132,17 @@ snapshots: optionalDependencies: expo-application: 57.0.3(expo@57.0.26) + '@walletconnect/react-native-compat@2.25.0(846135d3436568983c2ddd3320193643)': + dependencies: + '@react-native-async-storage/async-storage': 2.2.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)) + '@react-native-community/netinfo': 12.0.1(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + fast-text-encoding: 1.0.6 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + react-native-get-random-values: 1.11.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)) + react-native-url-polyfill: 2.0.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)) + optionalDependencies: + expo-application: 57.0.3(expo@57.0.26) + '@walletconnect/relay-api@1.0.11': dependencies: '@walletconnect/jsonrpc-types': 1.0.4 @@ -39389,7 +39799,6 @@ snapshots: slash: 3.0.0 transitivePeerDependencies: - supports-color - optional: true babel-jest@29.7.0(@babel/core@7.29.7(supports-color@5.5.0))(supports-color@5.5.0): dependencies: @@ -39427,7 +39836,6 @@ snapshots: test-exclude: 6.0.0 transitivePeerDependencies: - supports-color - optional: true babel-plugin-istanbul@6.1.1(supports-color@5.5.0): dependencies: @@ -39600,7 +40008,6 @@ snapshots: '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.7(supports-color@10.2.2)) '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.7(supports-color@10.2.2)) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.7(supports-color@10.2.2)) - optional: true babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.7(supports-color@5.5.0)): dependencies: @@ -39803,7 +40210,6 @@ snapshots: '@babel/core': 7.29.7(supports-color@10.2.2) babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7(supports-color@10.2.2)) - optional: true babel-preset-jest@29.6.3(@babel/core@7.29.7(supports-color@5.5.0)): dependencies: @@ -40311,6 +40717,13 @@ snapshots: dependencies: node-gyp-build: 4.8.4 + builder-util-runtime@9.7.0(supports-color@10.2.2): + dependencies: + debug: 4.4.3(supports-color@10.2.2) + sax: 1.6.1 + transitivePeerDependencies: + - supports-color + builder-util-runtime@9.7.0(supports-color@8.1.1): dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -40878,6 +41291,21 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.12 + create-jest@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@10.2.2): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@10.2.2) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + create-jest@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@8.1.1): dependencies: '@jest/types': 29.6.3 @@ -41615,9 +42043,9 @@ snapshots: electron-to-chromium@1.5.444: {} - electron-updater@6.8.9(supports-color@8.1.1): + electron-updater@6.8.9(supports-color@10.2.2): dependencies: - builder-util-runtime: 9.7.0(supports-color@8.1.1) + builder-util-runtime: 9.7.0(supports-color@10.2.2) fs-extra: 10.1.0 js-yaml: 4.3.2 lazy-val: 1.0.5 @@ -41628,6 +42056,18 @@ snapshots: transitivePeerDependencies: - supports-color + electron-vite@5.0.0(supports-color@10.2.2)(vite@7.3.6(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.51.2)(yaml@2.9.1)): + dependencies: + '@babel/core': 7.29.7(supports-color@10.2.2) + '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2)) + cac: 6.7.14 + esbuild: 0.25.12 + magic-string: 0.30.21 + picocolors: 1.1.1 + vite: 7.3.6(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.51.2)(yaml@2.9.1) + transitivePeerDependencies: + - supports-color + electron-vite@5.0.0(supports-color@8.1.1)(vite@7.3.6(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.51.2)(yaml@2.9.1)): dependencies: '@babel/core': 7.29.7(supports-color@8.1.1) @@ -41652,6 +42092,14 @@ snapshots: transitivePeerDependencies: - supports-color + electron@44.5.1(supports-color@10.2.2): + dependencies: + '@electron-internal/extract-zip': 1.0.5 + '@electron/get': 5.1.0(supports-color@10.2.2) + '@types/node': 24.19.1 + transitivePeerDependencies: + - supports-color + electron@44.5.1(supports-color@8.1.1): dependencies: '@electron-internal/extract-zip': 1.0.5 @@ -42117,6 +42565,17 @@ snapshots: dependencies: expo: 57.0.26(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo-asset@57.0.18(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3): + dependencies: + '@expo/image-utils': 0.11.5(supports-color@10.2.2)(typescript@5.9.3) + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo-constants: 57.0.20(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(supports-color@10.2.2) + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - supports-color + - typescript + expo-asset@57.0.18(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3): dependencies: '@expo/image-utils': 0.11.5(supports-color@10.2.2)(typescript@5.9.3) @@ -42151,6 +42610,13 @@ snapshots: - supports-color - typescript + expo-background-task@57.0.21(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)): + dependencies: + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo-task-manager: 57.0.21(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)) + transitivePeerDependencies: + - react-native + expo-background-task@57.0.21(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10)): dependencies: expo: 57.0.26(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -42158,6 +42624,15 @@ snapshots: transitivePeerDependencies: - react-native + expo-camera@57.0.6(@types/emscripten@1.41.6)(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3): + dependencies: + barcode-detector: 3.2.2(@types/emscripten@1.41.6) + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - '@types/emscripten' + expo-camera@57.0.6(@types/emscripten@1.41.6)(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3): dependencies: barcode-detector: 3.2.2(@types/emscripten@1.41.6) @@ -42167,12 +42642,26 @@ snapshots: transitivePeerDependencies: - '@types/emscripten' + expo-clipboard@57.0.2(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3): + dependencies: + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + expo-clipboard@57.0.2(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3): dependencies: expo: 57.0.26(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3)(utf-8-validate@5.0.10) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10) + expo-constants@57.0.20(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(supports-color@10.2.2): + dependencies: + '@expo/env': 2.4.3(supports-color@10.2.2) + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - supports-color + expo-constants@57.0.20(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(supports-color@10.2.2): dependencies: '@expo/env': 2.4.3(supports-color@10.2.2) @@ -42202,6 +42691,17 @@ snapshots: dependencies: expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.3.0))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo-dev-client@57.0.19(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)): + dependencies: + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo-dev-launcher: 57.0.20(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)) + expo-dev-menu: 57.0.18(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)) + expo-dev-menu-interface: 57.0.0(expo@57.0.26) + expo-manifests: 57.0.2(expo@57.0.26) + expo-updates-interface: 57.0.2(expo@57.0.26) + transitivePeerDependencies: + - react-native + expo-dev-client@57.0.19(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10)): dependencies: expo: 57.0.26(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -42213,6 +42713,14 @@ snapshots: transitivePeerDependencies: - react-native + expo-dev-launcher@57.0.20(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)): + dependencies: + '@expo/schema-utils': 57.0.2 + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo-dev-menu: 57.0.18(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)) + expo-manifests: 57.0.2(expo@57.0.26) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + expo-dev-launcher@57.0.20(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10)): dependencies: '@expo/schema-utils': 57.0.2 @@ -42225,6 +42733,12 @@ snapshots: dependencies: expo: 57.0.26(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo-dev-menu@57.0.18(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)): + dependencies: + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo-dev-menu-interface: 57.0.0(expo@57.0.26) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + expo-dev-menu@57.0.18(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10)): dependencies: expo: 57.0.26(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -42236,6 +42750,11 @@ snapshots: expo: 57.0.26(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3)(utf-8-validate@5.0.10) ua-parser-js: 0.7.41 + expo-file-system@57.0.7(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)): + dependencies: + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + expo-file-system@57.0.7(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)): dependencies: expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.3.0))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -42252,6 +42771,13 @@ snapshots: expo: 57.0.26(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3)(utf-8-validate@5.0.10) react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10) + expo-font@57.0.4(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3): + dependencies: + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + fontfaceobserver: 2.3.0 + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + expo-font@57.0.4(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0): dependencies: expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.3.0))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -42292,6 +42818,16 @@ snapshots: expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.3.0))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) react: 19.3.0 + expo-linking@57.0.11(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2): + dependencies: + expo-constants: 57.0.20(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(supports-color@10.2.2) + invariant: 2.2.4 + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - expo + - supports-color + expo-linking@57.0.11(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@8.1.1): dependencies: expo-constants: 57.0.20(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(supports-color@8.1.1) @@ -42349,6 +42885,14 @@ snapshots: - supports-color - typescript + expo-modules-core@57.0.20(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3): + dependencies: + '@expo/expo-modules-macros-plugin': 0.6.1 + expo-modules-jsi: 57.1.1(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)) + invariant: 2.2.4 + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + expo-modules-core@57.0.20(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0): dependencies: '@expo/expo-modules-macros-plugin': 0.6.1 @@ -42374,6 +42918,10 @@ snapshots: react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10) + expo-modules-jsi@57.1.1(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)): + dependencies: + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + expo-modules-jsi@57.1.1(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)): dependencies: react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10) @@ -42387,6 +42935,20 @@ snapshots: dependencies: react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10) + expo-notifications@57.0.21(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3): + dependencies: + '@expo/image-utils': 0.11.5(supports-color@10.2.2)(typescript@5.9.3) + abort-controller: 3.0.0 + badgin: 1.2.3 + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo-application: 57.0.3(expo@57.0.26) + expo-constants: 57.0.20(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(supports-color@10.2.2) + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - supports-color + - typescript + expo-notifications@57.0.21(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3): dependencies: '@expo/image-utils': 0.11.5(supports-color@8.1.1)(typescript@5.9.3) @@ -42407,6 +42969,16 @@ snapshots: expo-server@57.0.3: {} + expo-splash-screen@57.0.9(expo@57.0.26)(supports-color@10.2.2)(typescript@5.9.3): + dependencies: + '@expo/config-plugins': 57.0.9(supports-color@10.2.2)(typescript@5.9.3) + '@expo/image-utils': 0.11.5(supports-color@10.2.2)(typescript@5.9.3) + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + xml2js: 0.6.0 + transitivePeerDependencies: + - supports-color + - typescript + expo-splash-screen@57.0.9(expo@57.0.26)(supports-color@8.1.1)(typescript@5.9.3): dependencies: '@expo/config-plugins': 57.0.9(supports-color@8.1.1)(typescript@5.9.3) @@ -42417,12 +42989,24 @@ snapshots: - supports-color - typescript + expo-status-bar@57.0.1(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3): + dependencies: + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + expo-status-bar@57.0.1(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3): dependencies: expo: 57.0.26(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3)(utf-8-validate@5.0.10) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10) + expo-task-manager@57.0.21(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)): + dependencies: + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + unimodules-app-loader: 57.0.2 + expo-task-manager@57.0.21(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10)): dependencies: expo: 57.0.26(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -42433,6 +43017,11 @@ snapshots: dependencies: expo: 57.0.26(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo-web-browser@57.0.3(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)): + dependencies: + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + expo-web-browser@57.0.3(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)): dependencies: expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.3.0))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -42450,6 +43039,47 @@ snapshots: expo: 57.0.26(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3)(utf-8-validate@5.0.10) react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10) + expo@57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10): + dependencies: + '@babel/runtime': 7.29.7 + '@expo/cli': 57.0.27(6121b4fccc0cdac69a0d80123b73f485) + '@expo/config': 57.0.9(supports-color@10.2.2)(typescript@5.9.3) + '@expo/config-plugins': 57.0.9(supports-color@10.2.2)(typescript@5.9.3) + '@expo/devtools': 57.0.1(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + '@expo/fingerprint': 0.20.13(supports-color@10.2.2) + '@expo/local-build-cache-provider': 57.0.8(supports-color@10.2.2)(typescript@5.9.3) + '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + '@expo/metro': 56.0.2(bufferutil@4.1.0)(supports-color@10.2.2)(utf-8-validate@5.0.10) + '@expo/metro-config': 57.0.12(bufferutil@4.1.0)(expo@57.0.26)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@ungap/structured-clone': 1.4.0 + babel-preset-expo: 57.0.13(@babel/core@7.29.7(supports-color@10.2.2))(@babel/runtime@7.29.7)(expo@57.0.26)(react-refresh@0.14.2)(supports-color@10.2.2) + expo-asset: 57.0.18(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3) + expo-constants: 57.0.20(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(supports-color@10.2.2) + expo-file-system: 57.0.7(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)) + expo-font: 57.0.4(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + expo-keep-awake: 57.0.2(expo@57.0.26)(react@19.2.3) + expo-modules-autolinking: 57.0.13(supports-color@10.2.2)(typescript@5.9.3) + expo-modules-core: 57.0.20(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + pretty-format: 29.7.0 + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + react-refresh: 0.14.2 + whatwg-url-minimum: 0.1.2 + optionalDependencies: + '@expo/dom-webview': 57.0.1(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + react-dom: 19.3.0(react@19.2.3) + react-native-webview: 13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + transitivePeerDependencies: + - '@babel/core' + - bufferutil + - expo-router + - expo-widgets + - react-native-worklets + - react-server-dom-webpack + - supports-color + - typescript + - utf-8-validate + expo@57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.3.0))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10): dependencies: '@babel/runtime': 7.29.7 @@ -43234,11 +43864,11 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - http-proxy-agent@5.0.0(supports-color@8.1.1): + http-proxy-agent@5.0.0(supports-color@10.2.2): dependencies: '@tootallnate/once': 2.0.1 - agent-base: 6.0.2(supports-color@8.1.1) - debug: 4.4.3(supports-color@8.1.1) + agent-base: 6.0.2(supports-color@10.2.2) + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -43550,7 +44180,6 @@ snapshots: semver: 6.3.1 transitivePeerDependencies: - supports-color - optional: true istanbul-lib-instrument@5.2.1(supports-color@5.5.0): dependencies: @@ -43573,6 +44202,16 @@ snapshots: transitivePeerDependencies: - supports-color + istanbul-lib-instrument@6.0.3(supports-color@10.2.2): + dependencies: + '@babel/core': 7.29.7(supports-color@10.2.2) + '@babel/parser': 7.29.9 + '@istanbuljs/schema': 0.1.6 + istanbul-lib-coverage: 3.2.2 + semver: 7.8.5 + transitivePeerDependencies: + - supports-color + istanbul-lib-instrument@6.0.3(supports-color@8.1.1): dependencies: '@babel/core': 7.29.7(supports-color@8.1.1) @@ -43589,6 +44228,14 @@ snapshots: make-dir: 4.0.0 supports-color: 7.2.0 + istanbul-lib-source-maps@4.0.1(supports-color@10.2.2): + dependencies: + debug: 4.4.3(supports-color@10.2.2) + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + istanbul-lib-source-maps@4.0.1(supports-color@8.1.1): dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -43660,6 +44307,32 @@ snapshots: jest-util: 29.7.0 p-limit: 3.1.0 + jest-circus@29.7.0(babel-plugin-macros@3.1.0)(supports-color@10.2.2): + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0(supports-color@10.2.2) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.20.5 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.2(babel-plugin-macros@3.1.0) + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0(supports-color@10.2.2) + jest-snapshot: 29.7.0(supports-color@10.2.2) + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-circus@29.7.0(babel-plugin-macros@3.1.0)(supports-color@8.1.1): dependencies: '@jest/environment': 29.7.0 @@ -43686,6 +44359,25 @@ snapshots: - babel-plugin-macros - supports-color + jest-cli@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@10.2.2): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(supports-color@10.2.2) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@10.2.2) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@10.2.2) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.3 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest-cli@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@8.1.1): dependencies: '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(supports-color@8.1.1) @@ -43705,6 +44397,36 @@ snapshots: - supports-color - ts-node + jest-config@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@10.2.2): + dependencies: + '@babel/core': 7.29.7(supports-color@10.2.2) + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0)(supports-color@10.2.2) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0(supports-color@10.2.2) + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.20.5 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@8.1.1): dependencies: '@babel/core': 7.29.7(supports-color@8.1.1) @@ -43761,7 +44483,7 @@ snapshots: jest-util: 29.7.0 pretty-format: 29.7.0 - jest-environment-jsdom@29.7.0(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@5.0.10): + jest-environment-jsdom@29.7.0(bufferutil@4.1.0)(supports-color@10.2.2)(utf-8-validate@5.0.10): dependencies: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 @@ -43770,7 +44492,7 @@ snapshots: '@types/node': 22.20.5 jest-mock: 29.7.0 jest-util: 29.7.0 - jsdom: 20.0.3(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@5.0.10) + jsdom: 20.0.3(bufferutil@4.1.0)(supports-color@10.2.2)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - supports-color @@ -43785,24 +44507,24 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 - jest-expo@57.0.5(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(bufferutil@4.1.0)(expo@57.0.26)(jest@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@8.1.1))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10): + jest-expo@57.0.5(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(bufferutil@4.1.0)(expo@57.0.26)(jest@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@10.2.2))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10): dependencies: '@jest/create-cache-key-function': 29.7.0 - '@jest/globals': 29.7.0(supports-color@8.1.1) - '@react-native/jest-preset': 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) - babel-jest: 29.7.0(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) - jest-environment-jsdom: 29.7.0(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@5.0.10) - jest-snapshot: 29.7.0(supports-color@8.1.1) + '@jest/globals': 29.7.0(supports-color@10.2.2) + '@react-native/jest-preset': 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2) + babel-jest: 29.7.0(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) + jest-environment-jsdom: 29.7.0(bufferutil@4.1.0)(supports-color@10.2.2)(utf-8-validate@5.0.10) + jest-snapshot: 29.7.0(supports-color@10.2.2) jest-watch-select-projects: 2.0.0 - jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)) + jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@10.2.2)) json5: 2.2.3 lodash: 4.18.1 - react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) react-test-renderer: 19.2.3(react@19.2.3) server-only: 0.0.1 stacktrace-js: 2.0.2 optionalDependencies: - expo: 57.0.26(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.2.3))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - '@babel/core' - bufferutil @@ -43873,6 +44595,13 @@ snapshots: jest-regex-util@29.6.3: {} + jest-resolve-dependencies@29.7.0(supports-color@10.2.2): + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + jest-resolve-dependencies@29.7.0(supports-color@8.1.1): dependencies: jest-regex-util: 29.6.3 @@ -43892,6 +44621,32 @@ snapshots: resolve.exports: 2.0.3 slash: 3.0.0 + jest-runner@29.7.0(supports-color@10.2.2): + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0(supports-color@10.2.2) + '@jest/types': 29.6.3 + '@types/node': 22.20.5 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0(supports-color@10.2.2) + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + jest-runner@29.7.0(supports-color@8.1.1): dependencies: '@jest/console': 29.7.0 @@ -43918,6 +44673,33 @@ snapshots: transitivePeerDependencies: - supports-color + jest-runtime@29.7.0(supports-color@10.2.2): + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0(supports-color@10.2.2) + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0(supports-color@10.2.2) + '@jest/types': 29.6.3 + '@types/node': 22.20.5 + chalk: 4.1.2 + cjs-module-lexer: 1.4.3 + collect-v8-coverage: 1.0.3 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0(supports-color@10.2.2) + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + jest-runtime@29.7.0(supports-color@8.1.1): dependencies: '@jest/environment': 29.7.0 @@ -43945,6 +44727,31 @@ snapshots: transitivePeerDependencies: - supports-color + jest-snapshot@29.7.0(supports-color@10.2.2): + dependencies: + '@babel/core': 7.29.7(supports-color@10.2.2) + '@babel/generator': 7.29.8 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2)) + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2)) + '@babel/types': 7.29.8 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0(supports-color@10.2.2) + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7(supports-color@10.2.2)) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.8.5 + transitivePeerDependencies: + - supports-color + jest-snapshot@29.7.0(supports-color@8.1.1): dependencies: '@babel/core': 7.29.7(supports-color@8.1.1) @@ -43994,11 +44801,11 @@ snapshots: chalk: 3.0.0 prompts: 2.4.2 - jest-watch-typeahead@2.2.1(jest@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)): + jest-watch-typeahead@2.2.1(jest@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@10.2.2)): dependencies: ansi-escapes: 6.2.1 chalk: 4.1.2 - jest: 29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@8.1.1) + jest: 29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@10.2.2) jest-regex-util: 29.6.3 jest-watcher: 29.7.0 slash: 5.1.0 @@ -44023,6 +44830,18 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + jest@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@10.2.2): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(supports-color@10.2.2) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@10.2.2) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest@29.7.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(supports-color@8.1.1): dependencies: '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(supports-color@8.1.1) @@ -44080,7 +44899,7 @@ snapshots: jschardet@3.1.4: {} - jsdom@20.0.3(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@5.0.10): + jsdom@20.0.3(bufferutil@4.1.0)(supports-color@10.2.2)(utf-8-validate@5.0.10): dependencies: abab: 2.0.6 acorn: 8.18.0 @@ -44093,8 +44912,8 @@ snapshots: escodegen: 2.1.0 form-data: 4.0.6 html-encoding-sniffer: 3.0.0 - http-proxy-agent: 5.0.0(supports-color@8.1.1) - https-proxy-agent: 5.0.1(supports-color@8.1.1) + http-proxy-agent: 5.0.0(supports-color@10.2.2) + https-proxy-agent: 5.0.1(supports-color@10.2.2) is-potential-custom-element-name: 1.0.1 nwsapi: 2.2.28 parse5: 7.3.0 @@ -47268,11 +48087,21 @@ snapshots: react-native-argon2@4.0.0: {} + react-native-ble-plx@3.4.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3): + dependencies: + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + react-native-ble-plx@3.4.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3): dependencies: react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10) + react-native-get-random-values@1.11.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)): + dependencies: + fast-base64-decode: 1.0.0 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + react-native-get-random-values@1.11.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)): dependencies: fast-base64-decode: 1.0.0 @@ -47288,16 +48117,34 @@ snapshots: fast-base64-decode: 1.0.0 react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10) + react-native-passkey@3.6.2(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3): + dependencies: + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + react-native-passkey@3.6.2(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3): dependencies: react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10) + react-native-safe-area-context@5.7.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3): + dependencies: + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + react-native-safe-area-context@5.7.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3): dependencies: react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10) + react-native-svg@15.15.4(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3): + dependencies: + css-select: 5.2.2 + css-tree: 1.1.3 + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + warn-once: 0.1.1 + react-native-svg@15.15.4(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10))(react@19.2.3): dependencies: css-select: 5.2.2 @@ -47306,11 +48153,23 @@ snapshots: react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10) warn-once: 0.1.1 + react-native-url-polyfill@2.0.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10)): + dependencies: + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + whatwg-url-without-unicode: 8.0.0-3 + react-native-url-polyfill@2.0.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10)): dependencies: react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10) whatwg-url-without-unicode: 8.0.0-3 + react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3): + dependencies: + escape-string-regexp: 4.0.0 + invariant: 2.2.4 + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10) + react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0): dependencies: escape-string-regexp: 4.0.0 @@ -47334,6 +48193,52 @@ snapshots: react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@8.1.1)(utf-8-validate@5.0.10) + react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10): + dependencies: + '@react-native/assets-registry': 0.86.3 + '@react-native/codegen': 0.86.3(@babel/core@7.29.7(supports-color@10.2.2)) + '@react-native/community-cli-plugin': 0.86.3(bufferutil@4.1.0)(supports-color@10.2.2)(utf-8-validate@5.0.10) + '@react-native/gradle-plugin': 0.86.3 + '@react-native/js-polyfills': 0.86.3 + '@react-native/normalize-colors': 0.86.3 + '@react-native/virtualized-lists': 0.86.3(@types/react@19.2.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@types/react@19.2.18)(bufferutil@4.1.0)(react@19.2.3)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.2.3) + abort-controller: 3.0.0 + anser: 1.4.10 + ansi-regex: 5.0.1 + babel-plugin-syntax-hermes-parser: 0.36.0 + base64-js: 1.5.1 + commander: 12.1.0 + flow-enums-runtime: 0.0.6 + hermes-compiler: 250829098.0.17 + invariant: 2.2.4 + memoize-one: 5.2.1 + metro-runtime: 0.84.6 + metro-source-map: 0.84.6(supports-color@10.2.2) + nullthrows: 1.1.1 + pretty-format: 29.7.0 + promise: 8.3.0 + react: 19.2.3 + react-devtools-core: 6.1.5(bufferutil@4.1.0)(utf-8-validate@5.0.10) + react-refresh: 0.14.2 + regenerator-runtime: 0.13.11 + scheduler: 0.27.0 + semver: 7.8.5 + stacktrace-parser: 0.1.11 + tinyglobby: 0.2.17 + whatwg-fetch: 3.6.20 + ws: 7.5.13(bufferutil@4.1.0)(utf-8-validate@5.0.10) + yargs: 17.7.3 + optionalDependencies: + '@react-native/jest-preset': 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2) + '@types/react': 19.2.18 + transitivePeerDependencies: + - '@babel/core' + - '@react-native-community/cli' + - '@react-native/metro-config' + - bufferutil + - supports-color + - utf-8-validate + react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10): dependencies: '@react-native/assets-registry': 0.86.3 @@ -48904,6 +49809,12 @@ snapshots: tinyglobby: 0.2.17 ts-interface-checker: 0.1.13 + sumchecker@3.0.1(supports-color@10.2.2): + dependencies: + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + sumchecker@3.0.1(supports-color@8.1.1): dependencies: debug: 4.4.3(supports-color@8.1.1) diff --git a/templates/scaffold-hbar-clip-wallet/tools/harness/check.mjs b/templates/scaffold-hbar-clip-wallet/tools/harness/check.mjs index b72daf2..76762a0 100644 --- a/templates/scaffold-hbar-clip-wallet/tools/harness/check.mjs +++ b/templates/scaffold-hbar-clip-wallet/tools/harness/check.mjs @@ -36,10 +36,12 @@ export const VAULT_IMPORT_ALLOW = [ /^apps\/extension\/(?:src\/)?(?:entrypoints\/)?background(?:\/|\.[cm]?[jt]sx?$)/, // The extension background as a library (@clip-wallet/extension-kit), which every kit-built wallet runs. /^packages\/extension-kit\/src\/background\//, - // The mobile app's background (it builds the vault for @clip-wallet/engine, like the extension background). - /^apps\/mobile\/src\/background\//, - // The desktop app's main-process host (it builds the vault for @clip-wallet/engine; renderers and preloads never may). - /^apps\/desktop\/src\/main\/host\//, + // The phone app's background as a library (@clip-wallet/mobile-kit): it builds the vault for @clip-wallet/engine, + // like the extension background. + /^packages\/mobile-kit\/src\/background\//, + // The desktop app's main-process host as a library (@clip-wallet/desktop-kit): it builds the vault for + // @clip-wallet/engine; renderers and preloads never may. + /^packages\/desktop-kit\/src\/main\/host\//, // The desktop e2e's stand-in for the browser extension: an EMPTY vault, used only for ephemeral pairing keys. /^apps\/desktop\/e2e\/mock-extension\.ts$/, // The onboarding screen (packages/ui/src/screens/Onboarding.tsx) or an onboarding folder in the UI or extension. diff --git a/tools/brand/render.mjs b/tools/brand/render.mjs index 9398a89..89b18fb 100755 --- a/tools/brand/render.mjs +++ b/tools/brand/render.mjs @@ -128,13 +128,14 @@ try { } // The vector mark is what the wallet announces to dapps (EIP-6963 / Wallet Standard) and what configs point at. -for (const dest of ["apps/extension/icon.svg", "apps/mobile/assets/icon.svg", "apps/desktop/icon.svg", "apps/desktop/src/renderer/public/icon.svg"]) copyFileSync(join(ROOT, "brand/clip-mark.svg"), join(ROOT, dest)); +// The desktop and phone apps inline their clip.config icon (these files) as the dapp-facing identity at build time. +for (const dest of ["apps/extension/icon.svg", "apps/mobile/assets/icon.svg", "apps/desktop/icon.svg"]) copyFileSync(join(ROOT, "brand/clip-mark.svg"), join(ROOT, dest)); console.log("icon.svg copied to apps/extension, apps/mobile/assets and apps/desktop"); -// The same mark as a data URI wherever code needs it inline (1Mask's default identity, the dApp-side kit modules, -// the mobile in-app browser). tools/harness/test/brand.test.mjs checks they stay equal to brand/clip-mark.svg. +// The same mark as a data URI wherever code needs it inline (1Mask's default identity, the dApp-side kit modules). +// tools/harness/test/brand.test.mjs checks they stay equal to brand/clip-mark.svg. const DATA_URI = `data:image/svg+xml;base64,${Buffer.from(read("brand/clip-mark.svg")).toString("base64")}`; -for (const file of ["packages/1mask/src/shared/config.ts", "packages/kit-modules/src/shared.ts", "apps/mobile/src/env.ts", "apps/desktop/src/shared/app-config.ts"]) { +for (const file of ["packages/1mask/src/shared/config.ts", "packages/kit-modules/src/shared.ts"]) { const src = read(file); const next = src.replace(/"data:image\/svg\+xml;base64,[A-Za-z0-9+/=]+"/, JSON.stringify(DATA_URI)); if (next === src && !src.includes(DATA_URI)) throw new Error(`${file}: no inline icon found to replace`); diff --git a/tools/harness/check.mjs b/tools/harness/check.mjs index b72daf2..76762a0 100644 --- a/tools/harness/check.mjs +++ b/tools/harness/check.mjs @@ -36,10 +36,12 @@ export const VAULT_IMPORT_ALLOW = [ /^apps\/extension\/(?:src\/)?(?:entrypoints\/)?background(?:\/|\.[cm]?[jt]sx?$)/, // The extension background as a library (@clip-wallet/extension-kit), which every kit-built wallet runs. /^packages\/extension-kit\/src\/background\//, - // The mobile app's background (it builds the vault for @clip-wallet/engine, like the extension background). - /^apps\/mobile\/src\/background\//, - // The desktop app's main-process host (it builds the vault for @clip-wallet/engine; renderers and preloads never may). - /^apps\/desktop\/src\/main\/host\//, + // The phone app's background as a library (@clip-wallet/mobile-kit): it builds the vault for @clip-wallet/engine, + // like the extension background. + /^packages\/mobile-kit\/src\/background\//, + // The desktop app's main-process host as a library (@clip-wallet/desktop-kit): it builds the vault for + // @clip-wallet/engine; renderers and preloads never may. + /^packages\/desktop-kit\/src\/main\/host\//, // The desktop e2e's stand-in for the browser extension: an EMPTY vault, used only for ephemeral pairing keys. /^apps\/desktop\/e2e\/mock-extension\.ts$/, // The onboarding screen (packages/ui/src/screens/Onboarding.tsx) or an onboarding folder in the UI or extension. diff --git a/tools/harness/test/brand.test.mjs b/tools/harness/test/brand.test.mjs index bd55808..695487c 100644 --- a/tools/harness/test/brand.test.mjs +++ b/tools/harness/test/brand.test.mjs @@ -14,11 +14,12 @@ const mark = read("brand/clip-mark.svg"); const dataUri = `data:image/svg+xml;base64,${mark.toString("base64")}`; test("icon.svg copies match brand/clip-mark.svg", () => { - for (const p of ["apps/extension/icon.svg", "apps/mobile/assets/icon.svg", "apps/desktop/icon.svg", "apps/desktop/src/renderer/public/icon.svg"]) assert.ok(read(p).equals(mark), `${p}: run node tools/brand/render.mjs`); + // The desktop and phone apps inline their clip.config icon (these files) as the EIP-6963 identity at build time. + for (const p of ["apps/extension/icon.svg", "apps/mobile/assets/icon.svg", "apps/desktop/icon.svg"]) assert.ok(read(p).equals(mark), `${p}: run node tools/brand/render.mjs`); }); test("inline identity icons are the brand mark, not a placeholder", () => { - for (const p of ["packages/1mask/src/shared/config.ts", "packages/kit-modules/src/shared.ts", "apps/mobile/src/env.ts", "apps/desktop/src/shared/app-config.ts"]) { + for (const p of ["packages/1mask/src/shared/config.ts", "packages/kit-modules/src/shared.ts"]) { assert.ok(read(p).toString().includes(JSON.stringify(dataUri)), `${p}: run node tools/brand/render.mjs`); } }); diff --git a/tools/harness/test/check.test.mjs b/tools/harness/test/check.test.mjs index 83a9710..831cb46 100644 --- a/tools/harness/test/check.test.mjs +++ b/tools/harness/test/check.test.mjs @@ -108,9 +108,9 @@ test("vault importers: background, mobile/desktop hosts, onboarding screen and t "apps/extension/src/background/index.ts", "packages/ui/src/screens/Onboarding.tsx", "packages/ui/src/screens/onboarding/Create.tsx", - "apps/mobile/src/background/host.ts", - "apps/desktop/src/main/host/wallet.ts", - "apps/desktop/src/main/host/link.ts", + "packages/mobile-kit/src/background/host.ts", + "packages/desktop-kit/src/main/host/wallet.ts", + "packages/desktop-kit/src/main/host/link.ts", "apps/desktop/e2e/mock-extension.ts", "packages/extension-kit/src/background/wiring.ts", ]) assert.ok(allowed(f), f); @@ -119,21 +119,25 @@ test("vault importers: background, mobile/desktop hosts, onboarding screen and t "packages/route/src/onboarding.ts", "packages/chains-evm/src/onboarding.ts", "apps/extension/entrypoints/popup/main.tsx", - "apps/mobile/src/screens/Home.tsx", - "apps/mobile/src/browser/bridge.ts", + "packages/mobile-kit/src/screens/Home.tsx", + "packages/mobile-kit/src/browser/bridge.ts", "packages/engine/src/engine.ts", - "apps/desktop/src/main/index.ts", - "apps/desktop/src/main/browser/browser.ts", - "apps/desktop/src/preload/dapp.ts", - "apps/desktop/src/preload/wallet.ts", - "apps/desktop/src/renderer/shared/clients.ts", - "apps/desktop/src/main/hostile/wallet.ts", - "apps/desktop/src/main/native-hosts.ts", + "packages/desktop-kit/src/main/index.ts", + "packages/desktop-kit/src/main/browser/browser.ts", + "packages/desktop-kit/src/preload/dapp.ts", + "packages/desktop-kit/src/preload/wallet.ts", + "packages/desktop-kit/src/renderer/shared/clients.ts", + "packages/desktop-kit/src/main/hostile/wallet.ts", + "packages/desktop-kit/src/main/native-hosts.ts", "apps/desktop/e2e/desktop.spec.ts", "apps/desktop/e2e/link.spec.ts", "apps/desktop/e2e/mock-extension.tsx", "packages/extension-kit/src/pages/mount.tsx", "packages/extension-kit/src/wxt.ts", + "apps/desktop/src/main/index.ts", + "apps/mobile/index.ts", + "packages/desktop-kit/src/electron-vite.ts", + "packages/mobile-kit/src/screens/Onboarding.tsx", ]) assert.ok(!allowed(f), f); }); From 1584585866310833050ee3fdf436ddcfc281e4c1 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 20:31:15 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat(create-clip-wallet):=20every=20platfor?= =?UTF-8?q?m=20from=20one=20clip.config.ts=20=E2=80=94=20extension,=20desk?= =?UTF-8?q?top,=20mobile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `npx create-clip-wallet my-wallet` now makes the wallet on every platform: packages/extension (WXT on @clip-wallet/extension-kit), packages/desktop (Electron on @clip-wallet/desktop-kit) and packages/mobile (Expo on @clip-wallet/mobile-kit), plus packages/nextjs with --scaffold-hbar. Each depends only on published @clip-wallet/* packages; one clip.config.ts + wallet.identity.json at the project root drives all of them. - Template (templates/scaffold-hbar-clip-wallet): clip.config.ts, wallet.identity.json, the logo, MAINNET.md, .env and .keys/ move to the root; new packages/desktop (clipDesktop(), electronBuilderConfig(), one-line entrypoints, entitlements) and packages/mobile (expoConfig(), withClipWallet(), registerClipWallet(), eas.json); the dapp reads the identity from the root; root scripts dev:/desktop:/mobile:*; docs/signing.md (store accounts and code-signing variable names, no secrets); README, AGENTS.md, llms.txt, MAINNET.md, .harness, CI and the provenance check cover every platform, with platform: / only: markers. - CLI: --platforms, --scaffold-hbar, --id, --logo (alias --icon), --languages, prompts for each, per-platform next steps; `brand` command (pnpm wallet:brand). selectParts() removes unchosen folders, scripts and doc blocks, so --scaffold-hbar with every platform is still byte-for-byte the create-scaffold-hbar project. - Icons (src/icons.mjs, Node built-ins only): PNG decode/encode, premultiplied area-average resampling, ICO and ICNS writers; extension 16–128, macOS .icns, Windows .ico, Linux 16–1024, tray + macOS template, iOS icon (opaque, on the accent), Android adaptive + monochrome, splash. SVG logos rasterised once with @resvg/resvg-js, rsvg-convert, sips or ImageMagick, whichever is installed. A later accent change keeps a custom logo. - extension-kit: clipWallet({ configDir }) for a shared clip.config.ts; store zips named after the wallet. - Harness: kit projects are recognised by the root config; every present platform must build through its kit (clipWallet, clipDesktop + electronBuilderConfig, expoConfig + withClipWallet); appId may not be org.coldai.*. - Kit e2e (tools/kit/e2e.mjs): every platform from the tarballs — identity and icons; install, harness, types; extension build, manifest, extension id and EIP-6963 in Chromium; desktop build, electron-builder for the host (macOS arm64 .app + zip) with bundle id, name, scheme and icon checked, packaged app launched headless to onboarding; Expo config, expo export (iOS + Android) and expo prebuild with ids, name, scheme and icons checked; mainnet refused by the harness and all three builds; security floor; --dapp builds the Scaffold-HBAR dapp. Verified: pnpm -r typecheck, pnpm -r test, pnpm harness, pnpm pack-all, pnpm kit:e2e --dapp, pnpm kit:e2e:scaffold-hbar (create-scaffold-hbar@latest, projects identical), apps/desktop Playwright e2e (2/2). Co-Authored-By: Claude Opus 5.5 --- .changeset/create-clip-wallet-platforms.md | 16 + .github/workflows/ci.yml | 4 +- packages/create-clip-wallet/package.json | 8 +- .../scripts/bundle-template.mjs | 2 +- packages/create-clip-wallet/src/icons.mjs | 584 ++++++++++++++++++ packages/create-clip-wallet/src/identity.mjs | 139 +++-- packages/create-clip-wallet/src/index.mjs | 240 +++++-- packages/create-clip-wallet/src/listings.mjs | 8 +- packages/create-clip-wallet/src/template.mjs | 101 ++- .../create-clip-wallet/test/icons.test.ts | 197 ++++++ .../create-clip-wallet/test/scaffold.test.ts | 199 +++++- packages/desktop-kit/src/main/index.ts | 2 + packages/extension-kit/src/wxt.ts | 28 +- packages/extension-kit/test/wxt.test.ts | 20 +- .../{packages/extension => }/.env.example | 0 .../.github/scripts/check-scaffold.sh | 20 +- .../.github/workflows/ci.yaml | 23 +- .../.github/workflows/fresh-scaffold.yaml | 2 +- .../.harness/acceptance-contract.json | 2 +- .../.harness/spec.md | 6 +- .../.harness/validators/static.json | 10 +- templates/scaffold-hbar-clip-wallet/AGENTS.md | 97 ++- .../{packages/extension => }/MAINNET.md | 16 +- templates/scaffold-hbar-clip-wallet/NOTICE | 13 +- templates/scaffold-hbar-clip-wallet/README.md | 211 +++++-- .../{packages/extension => }/clip.config.ts | 20 +- .../scaffold-hbar-clip-wallet/docs/signing.md | 88 +++ .../{packages/extension => }/icon.svg | 0 templates/scaffold-hbar-clip-wallet/llms.txt | 73 ++- .../scaffold-hbar-clip-wallet/package.json | 24 +- .../packages/desktop/.gitignore | 2 + .../packages/desktop/README.md | 26 + .../build/entitlements.mac.inherit.plist | 11 + .../desktop/build/entitlements.mac.plist | 16 + .../packages/desktop/build/icon.icns | Bin 0 -> 69578 bytes .../packages/desktop/build/icon.ico | Bin 0 -> 9259 bytes .../packages/desktop/build/icon.png | Bin 0 -> 13115 bytes .../desktop/build/icons/1024x1024.png | Bin 0 -> 13115 bytes .../packages/desktop/build/icons/128x128.png | Bin 0 -> 1991 bytes .../packages/desktop/build/icons/16x16.png | Bin 0 -> 232 bytes .../packages/desktop/build/icons/256x256.png | Bin 0 -> 4710 bytes .../packages/desktop/build/icons/32x32.png | Bin 0 -> 421 bytes .../packages/desktop/build/icons/48x48.png | Bin 0 -> 624 bytes .../packages/desktop/build/icons/512x512.png | Bin 0 -> 7911 bytes .../packages/desktop/build/icons/64x64.png | Bin 0 -> 818 bytes .../desktop/electron-builder.config.cjs | 9 + .../packages/desktop/electron.vite.config.ts | 21 + .../packages/desktop/package.json | 42 ++ .../packages/desktop/src/main/index.ts | 2 + .../packages/desktop/src/preload/dapp.ts | 1 + .../packages/desktop/src/preload/wallet.ts | 1 + .../desktop/src/renderer/approval/index.html | 13 + .../desktop/src/renderer/approval/main.tsx | 3 + .../desktop/src/renderer/browser/index.html | 13 + .../desktop/src/renderer/browser/main.tsx | 3 + .../desktop/src/renderer/public/tray/tray.png | Bin 0 -> 232 bytes .../src/renderer/public/tray/tray@2x.png | Bin 0 -> 421 bytes .../src/renderer/public/tray/trayTemplate.png | Bin 0 -> 157 bytes .../renderer/public/tray/trayTemplate@2x.png | Bin 0 -> 275 bytes .../desktop/src/renderer/wallet/index.html | 13 + .../desktop/src/renderer/wallet/main.tsx | 3 + .../packages/desktop/tsconfig.json | 16 + .../packages/extension/README.md | 18 +- .../packages/extension/package.json | 3 - .../packages/extension/public/icon/128.png | Bin 1575 -> 1991 bytes .../packages/extension/public/icon/16.png | Bin 202 -> 232 bytes .../packages/extension/public/icon/32.png | Bin 367 -> 421 bytes .../packages/extension/public/icon/48.png | Bin 498 -> 624 bytes .../packages/extension/tsconfig.json | 2 +- .../packages/extension/wxt.config.ts | 11 +- .../packages/mobile/.gitignore | 5 + .../packages/mobile/README.md | 27 + .../packages/mobile/app.config.ts | 10 + .../packages/mobile/assets/adaptive-icon.png | Bin 0 -> 14598 bytes .../mobile/assets/adaptive-monochrome.png | Bin 0 -> 14598 bytes .../packages/mobile/assets/icon.png | Bin 0 -> 11620 bytes .../packages/mobile/assets/splash-icon.png | Bin 0 -> 10394 bytes .../packages/mobile/babel.config.js | 4 + .../packages/mobile/eas.json | 9 + .../packages/mobile/index.ts | 6 + .../packages/mobile/metro.config.js | 5 + .../packages/mobile/package.json | 67 ++ .../packages/mobile/tsconfig.json | 10 + .../packages/nextjs/package.json | 1 - .../packages/nextjs/scaffold.config.ts | 4 +- .../packages/nextjs/utils/wallet.ts | 7 +- .../pnpm-workspace.yaml | 4 + .../scaffold-hbar-clip-wallet/template.json | 73 ++- .../tools/harness/check.mjs | 57 +- .../tools/verify-provenance.mjs | 36 +- .../extension => }/wallet.identity.json | 0 tools/harness/check.mjs | 57 +- tools/harness/test/check.test.mjs | 31 +- tools/kit/e2e.mjs | 301 +++++++-- tools/kit/scaffold-hbar-e2e.mjs | 6 +- tools/release/normalize-manifests.mjs | 5 +- tools/release/sync-template.mjs | 4 +- 97 files changed, 2643 insertions(+), 468 deletions(-) create mode 100644 .changeset/create-clip-wallet-platforms.md create mode 100644 packages/create-clip-wallet/src/icons.mjs create mode 100644 packages/create-clip-wallet/test/icons.test.ts rename templates/scaffold-hbar-clip-wallet/{packages/extension => }/.env.example (100%) rename templates/scaffold-hbar-clip-wallet/{packages/extension => }/MAINNET.md (70%) rename templates/scaffold-hbar-clip-wallet/{packages/extension => }/clip.config.ts (52%) create mode 100644 templates/scaffold-hbar-clip-wallet/docs/signing.md rename templates/scaffold-hbar-clip-wallet/{packages/extension => }/icon.svg (100%) create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/.gitignore create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/README.md create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/build/entitlements.mac.inherit.plist create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/build/entitlements.mac.plist create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/build/icon.icns create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/build/icon.ico create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/build/icon.png create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/build/icons/1024x1024.png create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/build/icons/128x128.png create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/build/icons/16x16.png create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/build/icons/256x256.png create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/build/icons/32x32.png create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/build/icons/48x48.png create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/build/icons/512x512.png create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/build/icons/64x64.png create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/electron-builder.config.cjs create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/electron.vite.config.ts create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/package.json create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/src/main/index.ts create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/src/preload/dapp.ts create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/src/preload/wallet.ts create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/approval/index.html create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/approval/main.tsx create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/browser/index.html create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/browser/main.tsx create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/public/tray/tray.png create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/public/tray/tray@2x.png create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/public/tray/trayTemplate.png create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/public/tray/trayTemplate@2x.png create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/wallet/index.html create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/wallet/main.tsx create mode 100644 templates/scaffold-hbar-clip-wallet/packages/desktop/tsconfig.json create mode 100644 templates/scaffold-hbar-clip-wallet/packages/mobile/.gitignore create mode 100644 templates/scaffold-hbar-clip-wallet/packages/mobile/README.md create mode 100644 templates/scaffold-hbar-clip-wallet/packages/mobile/app.config.ts create mode 100644 templates/scaffold-hbar-clip-wallet/packages/mobile/assets/adaptive-icon.png create mode 100644 templates/scaffold-hbar-clip-wallet/packages/mobile/assets/adaptive-monochrome.png create mode 100644 templates/scaffold-hbar-clip-wallet/packages/mobile/assets/icon.png create mode 100644 templates/scaffold-hbar-clip-wallet/packages/mobile/assets/splash-icon.png create mode 100644 templates/scaffold-hbar-clip-wallet/packages/mobile/babel.config.js create mode 100644 templates/scaffold-hbar-clip-wallet/packages/mobile/eas.json create mode 100644 templates/scaffold-hbar-clip-wallet/packages/mobile/index.ts create mode 100644 templates/scaffold-hbar-clip-wallet/packages/mobile/metro.config.js create mode 100644 templates/scaffold-hbar-clip-wallet/packages/mobile/package.json create mode 100644 templates/scaffold-hbar-clip-wallet/packages/mobile/tsconfig.json rename templates/scaffold-hbar-clip-wallet/{packages/extension => }/wallet.identity.json (100%) diff --git a/.changeset/create-clip-wallet-platforms.md b/.changeset/create-clip-wallet-platforms.md new file mode 100644 index 0000000..faf4475 --- /dev/null +++ b/.changeset/create-clip-wallet-platforms.md @@ -0,0 +1,16 @@ +--- +"create-clip-wallet": patch +"@clip-wallet/extension-kit": patch +--- + +`npx create-clip-wallet my-wallet` makes the wallet on every platform from one `clip.config.ts` at the project root: +the browser extension (`packages/extension`), the desktop app (`packages/desktop`, Electron on +`@clip-wallet/desktop-kit`) and the phone app (`packages/mobile`, Expo on `@clip-wallet/mobile-kit`), plus the +Scaffold-HBAR dapp with `--scaffold-hbar`. New options: `--platforms`, `--id`, `--logo` (every platform's icons +rendered from one PNG or SVG with Node alone: extension sizes, `.icns`, `.ico`, Linux PNGs, tray, iOS, Android adaptive +and monochrome, splash), `--languages`, `--scaffold-hbar`; a `brand` command (`pnpm wallet:brand`); per-platform next +steps; `docs/signing.md` in every project (store accounts and code-signing variables, no secrets). The identity, the +config, the logo, `MAINNET.md`, `.env` and `.keys/` now live at the project root. + +`@clip-wallet/extension-kit`: `clipWallet({ configDir })` reads the icon, `MAINNET.md` and the wallet-wide `.env` next to +a shared `clip.config.ts`; `wxt zip` names the store zip after the wallet. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5d1c0c..a95d53c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,7 +129,7 @@ jobs: run: pnpm -r --filter "./services/**" test kit: - name: kit e2e (create-clip-wallet and the Scaffold-HBAR template, from tarballs) + name: kit e2e (create-clip-wallet on every platform, and the Scaffold-HBAR template, from tarballs) runs-on: ubuntu-24.04 timeout-minutes: 60 steps: @@ -146,7 +146,7 @@ jobs: run: pnpm --filter @clip-wallet/extension exec playwright install --with-deps chromium - name: Git identity for the scaffolded projects' first commit run: git config --global user.name "kit e2e" && git config --global user.email "kit-e2e@users.noreply.github.com" - - name: create-clip-wallet from the packed tarballs + - name: create-clip-wallet from the packed tarballs (extension, desktop, mobile) run: pnpm kit:e2e - name: Scaffold-HBAR template (reuses the tarballs) run: pnpm kit:e2e:scaffold-hbar --no-pack diff --git a/packages/create-clip-wallet/package.json b/packages/create-clip-wallet/package.json index 5b74428..f2ef939 100644 --- a/packages/create-clip-wallet/package.json +++ b/packages/create-clip-wallet/package.json @@ -1,7 +1,7 @@ { "name": "create-clip-wallet", "version": "0.1.0", - "description": "Scaffold your own wallet on the Clip Wallet kit: its own name, icon, extension id and rdns, testnet by default", + "description": "Scaffold your own wallet on the Clip Wallet kit, on every platform from one clip.config.ts: browser extension, desktop app (Electron) and phone app (Expo), with its own name, ids and icons; testnet by default", "keywords": [ "clip-wallet", "wallet", @@ -9,7 +9,11 @@ "hedera", "create", "template", - "browser-extension" + "browser-extension", + "electron", + "expo", + "desktop", + "mobile" ], "license": "Apache-2.0", "author": "ColdAI (https://coldai.org)", diff --git a/packages/create-clip-wallet/scripts/bundle-template.mjs b/packages/create-clip-wallet/scripts/bundle-template.mjs index 2abd4ce..0c209fd 100644 --- a/packages/create-clip-wallet/scripts/bundle-template.mjs +++ b/packages/create-clip-wallet/scripts/bundle-template.mjs @@ -12,7 +12,7 @@ if (process.argv.includes("--clean")) process.exit(0); const src = join(pkg, "..", "..", "templates", "scaffold-hbar-clip-wallet"); if (!existsSync(join(src, "template.json"))) throw new Error(`no template at ${src}`); -const SKIP = new Set(["node_modules", ".next", ".output", ".output-fixtures", ".wxt", ".keys", ".env", ".harness/runs"]); +const SKIP = new Set(["node_modules", ".next", ".output", ".output-fixtures", ".wxt", ".keys", ".env", ".harness/runs", ".expo"]); cpSync(src, out, { recursive: true, filter: (p) => !SKIP.has(basename(p)) && !/\.tsbuildinfo$/.test(p) && !/(?:^|\/)\.env\.(?!example$)[^/]+$/.test(p), diff --git a/packages/create-clip-wallet/src/icons.mjs b/packages/create-clip-wallet/src/icons.mjs new file mode 100644 index 0000000..6260f63 --- /dev/null +++ b/packages/create-clip-wallet/src/icons.mjs @@ -0,0 +1,584 @@ +// Every platform's icons from one logo, with Node built-ins only: +// PNG decode/encode (zlib), area-average resampling with premultiplied alpha, ICO (PNG entries, Windows Vista+) and +// ICNS (PNG entries, macOS 10.7+) containers. +// A .png logo needs nothing else. A .svg logo is rasterised once at 1024 px by the first tool found: +// @resvg/resvg-js (if installed in the project), rsvg-convert (librsvg), sips (macOS, built in), magick (ImageMagick). +// Without a logo, the starter mark (icon.mjs) is drawn directly at 1024 px. +// +// What gets written (iconPlan() lists it; paths relative to the project root): +// icon.svg | icon.png the logo clip.config.ts points at (EIP-6963 identity) +// packages/extension/public/icon/{16,32,48,128}.png the extension manifest's icons +// packages/desktop/build/icon.icns macOS (16…1024, Retina pairs) +// packages/desktop/build/icon.ico Windows (16…256) +// packages/desktop/build/icons/x.png Linux (16…1024) +// packages/desktop/build/icon.png 1024 px (window icon on Linux) +// packages/desktop/src/renderer/public/tray/*.png tray / menu bar (macOS template image in black) +// packages/mobile/assets/icon.png iOS / Expo icon: 1024 px, opaque (logo on the accent) +// packages/mobile/assets/adaptive-icon.png Android adaptive foreground (logo in the 66% safe zone) +// packages/mobile/assets/adaptive-monochrome.png Android 13+ themed icon (white silhouette) +// packages/mobile/assets/splash-icon.png splash screen image (on the accent colour) +import { execFileSync } from "node:child_process"; +import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { tmpdir } from "node:os"; +import { dirname, extname, join, resolve } from "node:path"; +import { deflateSync, inflateSync } from "node:zlib"; +import { iconSvg, rgb } from "./icon.mjs"; + +/** @typedef {{ width: number, height: number, data: Uint8Array }} Raster RGBA, straight (not premultiplied) alpha */ +/** @typedef {"extension" | "desktop" | "mobile"} Platform */ + +export const PLATFORMS = /** @type {const} */ (["extension", "desktop", "mobile"]); +export const EXTENSION_ICON_SIZES = [16, 32, 48, 128]; +export const LINUX_ICON_SIZES = [16, 32, 48, 64, 128, 256, 512, 1024]; +export const ICO_SIZES = [16, 24, 32, 48, 64, 128, 256]; +/** ICNS entry types with PNG payloads: [type, pixel size]. */ +export const ICNS_TYPES = /** @type {const} */ ([ + ["icp4", 16], + ["icp5", 32], + ["ic07", 128], + ["ic08", 256], + ["ic09", 512], + ["ic10", 1024], + ["ic11", 32], + ["ic12", 64], + ["ic13", 256], + ["ic14", 512], +]); + +/* ------------------------------------------------------------------ PNG */ + +const PNG_SIG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); +const CRC_TABLE = Array.from({ length: 256 }, (_, n) => { + let c = n; + for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + return c >>> 0; +}); +/** @param {Buffer} buf */ +function crc32(buf) { + let c = 0xffffffff; + for (const b of buf) c = /** @type {number} */ (CRC_TABLE[(c ^ b) & 0xff]) ^ (c >>> 8); + return (c ^ 0xffffffff) >>> 0; +} +/** @param {string} type @param {Buffer} data */ +function chunk(type, data) { + const len = Buffer.alloc(4); + len.writeUInt32BE(data.length); + const body = Buffer.concat([Buffer.from(type, "ascii"), data]); + const crc = Buffer.alloc(4); + crc.writeUInt32BE(crc32(body)); + return Buffer.concat([len, body, crc]); +} + +/** RGBA PNG (8-bit, filter 0 rows). @param {Raster} img */ +export function encodePng(img) { + const { width, height, data } = img; + const raw = Buffer.alloc((width * 4 + 1) * height); + for (let y = 0; y < height; y++) { + raw[y * (width * 4 + 1)] = 0; + Buffer.from(data.buffer, data.byteOffset + y * width * 4, width * 4).copy(raw, y * (width * 4 + 1) + 1); + } + const ihdr = Buffer.alloc(13); + ihdr.writeUInt32BE(width, 0); + ihdr.writeUInt32BE(height, 4); + ihdr[8] = 8; + ihdr[9] = 6; + return Buffer.concat([PNG_SIG, chunk("IHDR", ihdr), chunk("IDAT", deflateSync(raw, { level: 9 })), chunk("IEND", Buffer.alloc(0))]); +} + +/** + * Decode a PNG to RGBA: grey, grey+alpha, RGB, RGBA (8 or 16 bit) and palette (1/2/4/8 bit, with tRNS); + * non-interlaced. @param {Buffer} buf @returns {Raster} + */ +export function decodePng(buf) { + if (buf.length < 8 || !buf.subarray(0, 8).equals(PNG_SIG)) throw new Error("not a PNG file"); + let width = 0; + let height = 0; + let depth = 0; + let type = 0; + let interlace = 0; + /** @type {Buffer | undefined} */ + let palette; + /** @type {Buffer | undefined} */ + let trns; + const idat = []; + for (let o = 8; o < buf.length; ) { + const len = buf.readUInt32BE(o); + const t = buf.toString("ascii", o + 4, o + 8); + const d = buf.subarray(o + 8, o + 8 + len); + if (t === "IHDR") { + width = d.readUInt32BE(0); + height = d.readUInt32BE(4); + depth = /** @type {number} */ (d[8]); + type = /** @type {number} */ (d[9]); + interlace = /** @type {number} */ (d[12]); + } else if (t === "PLTE") palette = d; + else if (t === "tRNS") trns = d; + else if (t === "IDAT") idat.push(d); + else if (t === "IEND") break; + o += 12 + len; + } + if (!width || !height) throw new Error("PNG has no image header"); + if (interlace) throw new Error("interlaced PNGs aren't supported: export the logo again without interlacing"); + const channels = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }[type]; + if (!channels) throw new Error(`PNG colour type ${type} isn't supported`); + const bpp = Math.max(1, (channels * depth) >> 3); + const stride = Math.ceil((width * channels * depth) / 8); + const raw = inflateSync(Buffer.concat(idat)); + const lines = Buffer.alloc(stride * height); + let prev = Buffer.alloc(stride); + for (let y = 0; y < height; y++) { + const f = /** @type {number} */ (raw[y * (stride + 1)]); + const line = Buffer.from(raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1))); + for (let i = 0; i < stride; i++) { + const a = i >= bpp ? /** @type {number} */ (line[i - bpp]) : 0; + const b = /** @type {number} */ (prev[i]); + const c = i >= bpp ? /** @type {number} */ (prev[i - bpp]) : 0; + const x = /** @type {number} */ (line[i]); + if (f === 1) line[i] = (x + a) & 255; + else if (f === 2) line[i] = (x + b) & 255; + else if (f === 3) line[i] = (x + ((a + b) >> 1)) & 255; + else if (f === 4) { + const p = a + b - c; + const pa = Math.abs(p - a); + const pb = Math.abs(p - b); + const pc = Math.abs(p - c); + line[i] = (x + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c)) & 255; + } else if (f !== 0) throw new Error(`PNG filter ${f} isn't valid`); + } + line.copy(lines, y * stride); + prev = line; + } + const max = (1 << depth) - 1; + /** sample n of row y (0..max for depth < 8, 0..255 for 8, high byte for 16) */ + const sample = (/** @type {number} */ y, /** @type {number} */ n) => { + if (depth === 8) return /** @type {number} */ (lines[y * stride + n]); + if (depth === 16) return /** @type {number} */ (lines[y * stride + n * 2]); + const bit = n * depth; + return (/** @type {number} */ (lines[y * stride + (bit >> 3)]) >> (8 - depth - (bit & 7))) & max; + }; + const scale = depth < 8 ? 255 / max : 1; + const out = new Uint8Array(width * height * 4); + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const o = (y * width + x) * 4; + if (type === 3) { + const i = sample(y, x); + if (!palette || i * 3 + 2 >= palette.length) throw new Error("PNG palette index out of range"); + out[o] = /** @type {number} */ (palette[i * 3]); + out[o + 1] = /** @type {number} */ (palette[i * 3 + 1]); + out[o + 2] = /** @type {number} */ (palette[i * 3 + 2]); + out[o + 3] = trns && i < trns.length ? /** @type {number} */ (trns[i]) : 255; + } else if (type === 0 || type === 4) { + const g = Math.round(sample(y, x * channels) * scale); + out[o] = out[o + 1] = out[o + 2] = g; + out[o + 3] = type === 4 ? sample(y, x * 2 + 1) : trns && depth <= 8 && trns.readUInt16BE(0) === sample(y, x) ? 0 : 255; + } else { + out[o] = sample(y, x * channels); + out[o + 1] = sample(y, x * channels + 1); + out[o + 2] = sample(y, x * channels + 2); + out[o + 3] = type === 6 ? sample(y, x * 4 + 3) : 255; + } + } + } + return { width, height, data: out }; +} + +/* ------------------------------------------------------------------ raster operations */ + +/** @param {number} w @param {number} h @param {[number, number, number, number]} [fill] @returns {Raster} */ +export function canvas(w, h, fill = [0, 0, 0, 0]) { + const data = new Uint8Array(w * h * 4); + for (let i = 0; i < data.length; i += 4) data.set(fill, i); + return { width: w, height: h, data }; +} + +/** + * Resize with premultiplied alpha: an area average when shrinking (every source pixel counts by the area it covers), + * bilinear when growing. Separable: rows, then columns. @param {Raster} img @param {number} w @param {number} h + */ +export function resize(img, w, h) { + if (img.width === w && img.height === h) return { width: w, height: h, data: img.data.slice() }; + const pre = new Float64Array(img.width * img.height * 4); + for (let i = 0; i < img.width * img.height; i++) { + const a = /** @type {number} */ (img.data[i * 4 + 3]) / 255; + pre[i * 4] = /** @type {number} */ (img.data[i * 4]) * a; + pre[i * 4 + 1] = /** @type {number} */ (img.data[i * 4 + 1]) * a; + pre[i * 4 + 2] = /** @type {number} */ (img.data[i * 4 + 2]) * a; + pre[i * 4 + 3] = a * 255; + } + const horiz = pass(pre, img.width, img.height, w, true); + const both = pass(horiz, w, img.height, h, false); + const data = new Uint8Array(w * h * 4); + for (let i = 0; i < w * h; i++) { + const a = /** @type {number} */ (both[i * 4 + 3]); + data[i * 4 + 3] = clamp(a); + const k = a > 0 ? 255 / a : 0; + data[i * 4] = clamp(/** @type {number} */ (both[i * 4]) * k); + data[i * 4 + 1] = clamp(/** @type {number} */ (both[i * 4 + 1]) * k); + data[i * 4 + 2] = clamp(/** @type {number} */ (both[i * 4 + 2]) * k); + } + return { width: w, height: h, data }; +} + +/** @param {number} v */ +function clamp(v) { + return v <= 0 ? 0 : v >= 255 ? 255 : Math.round(v); +} + +/** + * One resampling pass along x (horizontal) or y. @param {Float64Array} src @param {number} sw @param {number} sh + * @param {number} n target length along the pass @param {boolean} horizontal + */ +function pass(src, sw, sh, n, horizontal) { + const len = horizontal ? sw : sh; + const lines = horizontal ? sh : sw; + const out = new Float64Array((horizontal ? n * sh : sw * n) * 4); + const at = (/** @type {number} */ line, /** @type {number} */ i) => (horizontal ? line * sw + i : i * sw + line) * 4; + const to = (/** @type {number} */ line, /** @type {number} */ i) => (horizontal ? line * n + i : i * sw + line) * 4; + const ratio = len / n; + for (let line = 0; line < lines; line++) { + for (let i = 0; i < n; i++) { + const acc = [0, 0, 0, 0]; + if (ratio >= 1) { + const s0 = i * ratio; + const s1 = s0 + ratio; + for (let s = Math.floor(s0); s < Math.ceil(s1); s++) { + const wgt = Math.min(s + 1, s1) - Math.max(s, s0); + const p = at(line, Math.min(s, len - 1)); + for (let c = 0; c < 4; c++) acc[c] = /** @type {number} */ (acc[c]) + /** @type {number} */ (src[p + c]) * wgt; + } + for (let c = 0; c < 4; c++) acc[c] = /** @type {number} */ (acc[c]) / ratio; + } else { + const x = Math.max(0, Math.min(len - 1, (i + 0.5) * ratio - 0.5)); + const x0 = Math.floor(x); + const x1 = Math.min(len - 1, x0 + 1); + const f = x - x0; + for (let c = 0; c < 4; c++) acc[c] = /** @type {number} */ (src[at(line, x0) + c]) * (1 - f) + /** @type {number} */ (src[at(line, x1) + c]) * f; + } + out.set(acc, to(line, i)); + } + } + return out; +} + +/** Draw `top` over `base` at (x, y) (source-over). Mutates base. @param {Raster} base @param {Raster} top @param {number} x @param {number} y */ +export function over(base, top, x, y) { + for (let ty = 0; ty < top.height; ty++) { + const by = ty + y; + if (by < 0 || by >= base.height) continue; + for (let tx = 0; tx < top.width; tx++) { + const bx = tx + x; + if (bx < 0 || bx >= base.width) continue; + const t = (ty * top.width + tx) * 4; + const b = (by * base.width + bx) * 4; + const ta = /** @type {number} */ (top.data[t + 3]) / 255; + const ba = /** @type {number} */ (base.data[b + 3]) / 255; + const oa = ta + ba * (1 - ta); + for (let c = 0; c < 3; c++) { + base.data[b + c] = oa ? clamp((/** @type {number} */ (top.data[t + c]) * ta + /** @type {number} */ (base.data[b + c]) * ba * (1 - ta)) / oa) : 0; + } + base.data[b + 3] = clamp(oa * 255); + } + } + return base; +} + +/** + * The logo scaled to fit a `box`-pixel square (keeping its aspect ratio), centred on a size×size canvas, over an optional + * opaque background. @param {Raster} logo @param {number} size @param {number} box @param {string} [background] + */ +export function fit(logo, size, box, background) { + const k = Math.min(box / logo.width, box / logo.height); + const w = Math.max(1, Math.round(logo.width * k)); + const h = Math.max(1, Math.round(logo.height * k)); + const base = canvas(size, size, background ? [...rgb(background), 255] : [0, 0, 0, 0]); + return over(base, resize(logo, w, h), Math.round((size - w) / 2), Math.round((size - h) / 2)); +} + +/** Does the image have no meaningful transparency (fewer than 1% of pixels below alpha 250)? @param {Raster} img */ +export function isOpaque(img) { + let clear = 0; + for (let i = 3; i < img.data.length; i += 4) if (/** @type {number} */ (img.data[i]) < 250) clear++; + return clear < (img.width * img.height) / 100; +} + +/** + * A one-colour silhouette of the logo, for Android's themed icon and the macOS menu-bar template. The shape is the + * logo's alpha; an opaque logo (artwork on a solid background) uses its difference from the corner colour instead. + * @param {Raster} img @param {string} color + */ +export function silhouette(img, color) { + const [r, g, b] = rgb(color); + const out = canvas(img.width, img.height); + const opaque = isOpaque(img); + const bg = [img.data[0], img.data[1], img.data[2]].map(Number); + for (let i = 0; i < img.width * img.height; i++) { + const o = i * 4; + let a = /** @type {number} */ (img.data[o + 3]) / 255; + if (opaque) { + const d = Math.max(...[0, 1, 2].map((c) => Math.abs(/** @type {number} */ (img.data[o + c]) - /** @type {number} */ (bg[c])))) / 255; + a = Math.min(1, Math.max(0, (d - 0.08) / 0.25)); + } + out.data.set([r, g, b, clamp(a * 255)], o); + } + return out; +} + +/* ------------------------------------------------------------------ containers */ + +/** Windows .ico with PNG entries. @param {{ size: number, png: Buffer }[]} entries */ +export function encodeIco(entries) { + const head = Buffer.alloc(6 + entries.length * 16); + head.writeUInt16LE(0, 0); + head.writeUInt16LE(1, 2); + head.writeUInt16LE(entries.length, 4); + let offset = head.length; + entries.forEach((e, i) => { + const o = 6 + i * 16; + head[o] = e.size >= 256 ? 0 : e.size; + head[o + 1] = e.size >= 256 ? 0 : e.size; + head[o + 2] = 0; + head[o + 3] = 0; + head.writeUInt16LE(1, o + 4); + head.writeUInt16LE(32, o + 6); + head.writeUInt32LE(e.png.length, o + 8); + head.writeUInt32LE(offset, o + 12); + offset += e.png.length; + }); + return Buffer.concat([head, ...entries.map((e) => e.png)]); +} + +/** Read an .ico's directory (tests). @param {Buffer} buf */ +export function readIco(buf) { + const n = buf.readUInt16LE(4); + return Array.from({ length: n }, (_, i) => { + const o = 6 + i * 16; + const size = buf[o] || 256; + const len = buf.readUInt32LE(o + 8); + const at = buf.readUInt32LE(o + 12); + return { size, png: buf.subarray(at, at + len) }; + }); +} + +/** macOS .icns with PNG entries. @param {{ type: string, png: Buffer }[]} entries */ +export function encodeIcns(entries) { + const parts = entries.map((e) => { + const h = Buffer.alloc(8); + h.write(e.type, 0, "ascii"); + h.writeUInt32BE(e.png.length + 8, 4); + return Buffer.concat([h, e.png]); + }); + const total = 8 + parts.reduce((s, p) => s + p.length, 0); + const h = Buffer.alloc(8); + h.write("icns", 0, "ascii"); + h.writeUInt32BE(total, 4); + return Buffer.concat([h, ...parts]); +} + +/** Read an .icns's entries (tests). @param {Buffer} buf */ +export function readIcns(buf) { + if (buf.toString("ascii", 0, 4) !== "icns") throw new Error("not an icns file"); + const out = []; + for (let o = 8; o < buf.readUInt32BE(4); ) { + const len = buf.readUInt32BE(o + 4); + out.push({ type: buf.toString("ascii", o, o + 4), png: buf.subarray(o + 8, o + len) }); + o += len; + } + return out; +} + +/* ------------------------------------------------------------------ sources */ + +/** + * Rasterise an SVG at `size` px with the first tool available. Returns the PNG bytes. + * @param {string} file @param {number} size @param {{ cwd?: string }} [opts] + */ +export function rasterizeSvg(file, size, opts = {}) { + const svg = readFileSync(file); + // 1. @resvg/resvg-js, if the project (or this CLI) has it installed. Optional; never a dependency. + for (const from of [opts.cwd, process.cwd(), dirname(new URL(import.meta.url).pathname)].filter(Boolean)) { + try { + const req = createRequire(join(/** @type {string} */ (from), "package.json")); + const { Resvg } = req("@resvg/resvg-js"); + return Buffer.from(new Resvg(svg, { fitTo: { mode: "width", value: size } }).render().asPng()); + } catch { + /* not installed here */ + } + } + const tmp = mkdtempSync(join(tmpdir(), "clip-logo-")); + const out = join(tmp, "logo.png"); + const tools = [ + ["rsvg-convert", ["-w", String(size), "-h", String(size), "-a", "-o", out, file]], + ["sips", ["-s", "format", "png", "-Z", String(size), file, "--out", out]], + ["magick", ["-background", "none", "-density", "384", file, "-resize", `${size}x${size}`, out]], + ]; + try { + for (const [cmd, args] of tools) { + try { + execFileSync(/** @type {string} */ (cmd), /** @type {string[]} */ (args), { stdio: "ignore" }); + if (existsSync(out)) return readFileSync(out); + } catch { + /* next tool */ + } + } + } finally { + rmSync(tmp, { recursive: true, force: true }); + } + throw new Error( + `Can't turn ${file} into PNG icons here: export your logo as a 1024×1024 PNG and pass that (--logo logo.png), or install one of ` + + "@resvg/resvg-js (pnpm add -D @resvg/resvg-js), rsvg-convert (librsvg) or ImageMagick.", + ); +} + +/** + * The starter mark at `size` px: the whole mark, and just the white ring and dot (for adaptive / splash / template). + * @param {string} accent @param {number} [size] + */ +export function starterRasters(accent, size = 1024) { + const full = canvas(size, size); + const glyph = canvas(size, size); + const [ar, ag, ab] = rgb(accent); + const S = 2; + const R = 28; + for (let py = 0; py < size; py++) { + for (let px = 0; px < size; px++) { + let sq = 0; + let mk = 0; + for (let sy = 0; sy < S; sy++) { + for (let sx = 0; sx < S; sx++) { + const x = ((px + (sx + 0.5) / S) * 128) / size; + const y = ((py + (sy + 0.5) / S) * 128) / size; + const cx = Math.min(Math.max(x, R), 128 - R); + const cy = Math.min(Math.max(y, R), 128 - R); + const inSq = (x - cx) ** 2 + (y - cy) ** 2 <= R * R; + const d = Math.hypot(x - 64, y - 64); + const inMark = (d <= 40 && d >= 27) || d <= 10; + if (inSq) sq++; + if (inMark) mk++; + } + } + const o = (py * size + px) * 4; + const a = sq / (S * S); + const m = mk / (S * S); + const mixed = sq ? Math.min(1, mk / sq) : 0; + full.data.set([clamp(ar + (255 - ar) * mixed), clamp(ag + (255 - ag) * mixed), clamp(ab + (255 - ab) * mixed), clamp(a * 255)], o); + glyph.data.set([255, 255, 255, clamp(m * 255)], o); + } + } + return { full, glyph }; +} + +/** + * The logo as rasters: `full` (the logo itself) and `glyph` (what sits on a coloured background: the starter's white + * mark, or the logo again). @param {{ logo?: string, accent: string, cwd?: string }} o + */ +export function logoRasters(o) { + if (!o.logo) return { ...starterRasters(o.accent), starter: true, warnings: /** @type {string[]} */ ([]) }; + const ext = extname(o.logo).toLowerCase(); + const png = ext === ".svg" ? rasterizeSvg(o.logo, 1024, { cwd: o.cwd }) : readFileSync(o.logo); + const full = decodePng(png); + const warnings = []; + if (Math.min(full.width, full.height) < 1024) warnings.push(`${o.logo} is ${full.width}×${full.height}: icons above that size are scaled up; a 1024×1024 logo looks sharper`); + if (full.width !== full.height) warnings.push(`${o.logo} isn't square (${full.width}×${full.height}): it is centred on a square`); + return { full, glyph: full, starter: false, warnings }; +} + +/* ------------------------------------------------------------------ the plan */ + +/** + * Every icon file for the chosen platforms: [path, description] (paths relative to the project root). Pure. + * @param {readonly Platform[]} platforms @param {{ logoExt?: ".svg" | ".png" }} [o] + */ +export function iconPlan(platforms, o = {}) { + /** @type {{ file: string, platform: Platform | "wallet", what: string }[]} */ + const plan = [{ file: `icon${o.logoExt ?? ".svg"}`, platform: "wallet", what: "the logo clip.config.ts points at" }]; + if (platforms.includes("extension")) { + for (const s of EXTENSION_ICON_SIZES) plan.push({ file: `packages/extension/public/icon/${s}.png`, platform: "extension", what: `${s}×${s} manifest icon` }); + } + if (platforms.includes("desktop")) { + plan.push({ file: "packages/desktop/build/icon.icns", platform: "desktop", what: "macOS app icon (16–1024 px)" }); + plan.push({ file: "packages/desktop/build/icon.ico", platform: "desktop", what: "Windows app icon (16–256 px)" }); + plan.push({ file: "packages/desktop/build/icon.png", platform: "desktop", what: "1024×1024 window icon" }); + for (const s of LINUX_ICON_SIZES) plan.push({ file: `packages/desktop/build/icons/${s}x${s}.png`, platform: "desktop", what: `Linux ${s}×${s}` }); + for (const [f, s] of [["tray.png", 16], ["tray@2x.png", 32], ["trayTemplate.png", 16], ["trayTemplate@2x.png", 32]]) { + plan.push({ file: `packages/desktop/src/renderer/public/tray/${f}`, platform: "desktop", what: `${s}×${s} ${String(f).startsWith("trayTemplate") ? "macOS menu-bar template" : "tray"} icon` }); + } + } + if (platforms.includes("mobile")) { + plan.push({ file: "packages/mobile/assets/icon.png", platform: "mobile", what: "1024×1024 iOS / Expo icon (opaque)" }); + plan.push({ file: "packages/mobile/assets/adaptive-icon.png", platform: "mobile", what: "1024×1024 Android adaptive foreground" }); + plan.push({ file: "packages/mobile/assets/adaptive-monochrome.png", platform: "mobile", what: "1024×1024 Android themed (monochrome) icon" }); + plan.push({ file: "packages/mobile/assets/splash-icon.png", platform: "mobile", what: "1024×1024 splash image" }); + } + return plan; +} + +/** + * Write every icon for the chosen platforms into the project at `root`, from one logo (or the starter mark in the + * accent colour). Returns the files written and any warnings about the logo. + * @param {string} root @param {{ logo?: string, accent: string, platforms: readonly Platform[] }} o + */ +export function renderIcons(root, o) { + const logoExt = o.logo ? /** @type {".svg" | ".png"} */ (extname(o.logo).toLowerCase()) : ".svg"; + const src = logoRasters({ logo: o.logo ? resolve(o.logo) : undefined, accent: o.accent, cwd: root }); + /** @type {string[]} */ + const written = []; + const put = (/** @type {string} */ rel, /** @type {Buffer | string} */ data) => { + const f = join(root, rel); + mkdirSync(dirname(f), { recursive: true }); + writeFileSync(f, data); + written.push(rel); + }; + const png = (/** @type {Raster} */ img, /** @type {number} */ s) => encodePng(img.width === s && img.height === s ? img : fit(img, s, s)); + + mkdirSync(root, { recursive: true }); + // The logo itself, next to clip.config.ts (the other icon file name is removed so only one is in use). + const other = join(root, logoExt === ".svg" ? "icon.png" : "icon.svg"); + if (existsSync(other)) rmSync(other); + if (o.logo) { + if (resolve(o.logo) !== resolve(root, `icon${logoExt}`)) copyFileSync(resolve(o.logo), join(root, `icon${logoExt}`)); + written.push(`icon${logoExt}`); + } else put("icon.svg", iconSvg(o.accent)); + + const full = src.full; + // Downscale once from 1024, then from the nearest larger size (sharper small sizes, faster). + const sized = new Map(); + const at = (/** @type {number} */ s) => { + if (!sized.has(s)) sized.set(s, fit(full, s, s)); + return sized.get(s); + }; + + if (o.platforms.includes("extension")) for (const s of EXTENSION_ICON_SIZES) put(`packages/extension/public/icon/${s}.png`, png(at(s), s)); + + if (o.platforms.includes("desktop")) { + // macOS: the artwork on Apple's 824 px grid inside the 1024 canvas (the system adds no padding of its own). + const mac = fit(full, 1024, 824); + const macAt = new Map(); + const macPng = (/** @type {number} */ s) => { + if (!macAt.has(s)) macAt.set(s, encodePng(resize(mac, s, s))); + return macAt.get(s); + }; + put("packages/desktop/build/icon.icns", encodeIcns(ICNS_TYPES.map(([type, s]) => ({ type, png: macPng(s) })))); + put("packages/desktop/build/icon.ico", encodeIco(ICO_SIZES.map((s) => ({ size: s, png: encodePng(at(s)) })))); + put("packages/desktop/build/icon.png", encodePng(at(1024))); + for (const s of LINUX_ICON_SIZES) put(`packages/desktop/build/icons/${s}x${s}.png`, encodePng(at(s))); + put("packages/desktop/src/renderer/public/tray/tray.png", encodePng(at(16))); + put("packages/desktop/src/renderer/public/tray/tray@2x.png", encodePng(at(32))); + const template = silhouette(src.starter ? src.glyph : full, "#000000"); + put("packages/desktop/src/renderer/public/tray/trayTemplate.png", encodePng(fit(template, 16, 16))); + put("packages/desktop/src/renderer/public/tray/trayTemplate@2x.png", encodePng(fit(template, 32, 32))); + } + + if (o.platforms.includes("mobile")) { + // iOS rejects transparency and masks the corners itself: an opaque logo fills the square; otherwise the logo (the + // starter's white mark) sits on the accent colour. + const ios = !src.starter && isOpaque(full) ? fit(full, 1024, 1024) : fit(src.glyph, 1024, src.starter ? 1024 : 820, o.accent); + put("packages/mobile/assets/icon.png", encodePng(ios)); + // Android adaptive icons: the artwork inside the 66% safe zone (≈ 676 of 1024 px); the background is the accent. + put("packages/mobile/assets/adaptive-icon.png", encodePng(fit(src.glyph, 1024, 600))); + put("packages/mobile/assets/adaptive-monochrome.png", encodePng(fit(silhouette(src.glyph, "#FFFFFF"), 1024, 600))); + put("packages/mobile/assets/splash-icon.png", encodePng(fit(src.glyph, 1024, 1024))); + } + return { written, warnings: src.warnings }; +} diff --git a/packages/create-clip-wallet/src/identity.mjs b/packages/create-clip-wallet/src/identity.mjs index e46514a..194b413 100644 --- a/packages/create-clip-wallet/src/identity.mjs +++ b/packages/create-clip-wallet/src/identity.mjs @@ -1,23 +1,29 @@ -// A wallet's own identity, written into a project made from the Scaffold-HBAR Clip Wallet template: -// packages/extension/wallet.identity.json name, description, rdns, homepage, icon, extension.key (public) -// packages/extension/.keys/extension.pem the extension's private key (gitignored, 0600; never printed) -// packages/extension/clip.config.ts theme accent and networks (only when given) -// packages/extension/icon.svg + public/icon/{16,32,48,128}.png a starter icon in the accent colour +// A wallet's own identity, written into a project made from the Scaffold-HBAR Clip Wallet template. One clip.config.ts +// at the project root drives every platform (packages/extension, packages/desktop, packages/mobile): +// wallet.identity.json name, description, rdns, homepage, icon, appId, extension.key (public) +// .keys/extension.pem the extension's private key (gitignored, 0600; never printed) +// clip.config.ts theme accent, networks and languages (only when given) +// icon.svg | icon.png + every platform's icons (icons.mjs: extension, macOS/Windows/Linux, iOS/Android, splash) // packages/extension/src/entrypoints/{popup,tab}/index.html page titles -// packages/extension/.env, packages/nextjs/.env.local WalletConnect project id (never committed) +// .env, packages/nextjs/.env.local WalletConnect project id (never committed) // docs/listings/*.md listing-submission drafts for this identity // `npx create-clip-wallet my-wallet` runs it after copying the template; in a project made with create-scaffold-hbar, // `pnpm wallet:identity` runs the same code. import { createHash, generateKeyPairSync } from "node:crypto"; -import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { basename, extname, join, resolve } from "node:path"; +import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { basename, extname, join } from "node:path"; import { CLIP_WALLET_RDNS, WALLETCONNECT_ENV, validateConfig, walletKey } from "@clip-wallet/config"; -import { ICON_SIZES, iconPng, iconSvg } from "./icon.mjs"; +import { renderIcons } from "./icons.mjs"; import { writeListings } from "./listings.mjs"; +export const CONFIG_FILE = "clip.config.ts"; +export const IDENTITY_FILE = "wallet.identity.json"; +export const KEY_FILE = join(".keys", "extension.pem"); +export const CHECKLIST_FILE = "MAINNET.md"; export const EXTENSION_DIR = join("packages", "extension"); -export const IDENTITY_FILE = join(EXTENSION_DIR, "wallet.identity.json"); -export const KEY_FILE = join(EXTENSION_DIR, ".keys", "extension.pem"); +export const NEXT_DIR = join("packages", "nextjs"); +/** Where each platform lives in a project. */ +export const PLATFORM_DIRS = /** @type {const} */ ({ extension: join("packages", "extension"), desktop: join("packages", "desktop"), mobile: join("packages", "mobile") }); export const DEFAULT_ACCENT = "#4F46E5"; export const DEFAULT_NETWORKS = ["evm:*", "hedera", "solana", "bitcoin"]; @@ -42,7 +48,7 @@ export function placeholderRdns(slug) { return `com.example.${slug.replace(/[^a-z0-9]/g, "") || "wallet"}`; } -/** "evm:*, hedera" -> ["evm:*", "hedera"] @param {string} text */ +/** "evm:*, hedera" -> ["evm:*", "hedera"] (also languages, platforms). @param {string} text */ export function parseNetworks(text) { return text .split(/[\s,]+/) @@ -58,9 +64,14 @@ export function extensionIdFromKey(key) { return id; } +/** The platforms a project has (by folder). @param {string} root @returns {("extension" | "desktop" | "mobile")[]} */ +export function projectPlatforms(root) { + return /** @type {const} */ (["extension", "desktop", "mobile"]).filter((p) => existsSync(join(root, PLATFORM_DIRS[p], "package.json"))); +} + /** - * @typedef {{ name: string, rdns: string, accent?: string, networks?: string[], description?: string, homepage?: string, - * icon?: string, walletConnectProjectId?: string, newKey?: boolean }} IdentityAnswers + * @typedef {{ name: string, rdns: string, accent?: string, networks?: string[], languages?: string[], appId?: string, + * description?: string, homepage?: string, icon?: string, walletConnectProjectId?: string, newKey?: boolean }} IdentityAnswers */ /** @@ -76,16 +87,24 @@ export function checkIdentity(a) { ...(a.homepage ? { homepage: a.homepage } : {}), ...(a.accent ? { theme: { accent: a.accent } } : {}), ...(a.networks ? { networks: a.networks } : {}), + ...(a.languages ? { languages: a.languages } : {}), + ...(a.appId ? { appId: a.appId } : {}), ...(a.walletConnectProjectId ? { walletConnect: { projectId: a.walletConnectProjectId } } : {}), }); const problems = r.ok ? [] : [...r.problems]; if (a.rdns === CLIP_WALLET_RDNS || /^org\.coldai\./.test(a.rdns)) problems.push(`rdns: ${a.rdns} belongs to Clip Wallet; use a reverse domain you own`); + if (a.appId && /^org\.coldai\./.test(a.appId)) problems.push(`appId: ${a.appId} belongs to Clip Wallet; use a reverse domain you own`); if (a.name.trim().toLowerCase() === "clip wallet") problems.push("name: Clip Wallet is taken; give your wallet its own name"); - if (a.icon && !existsSync(a.icon)) problems.push(`icon: ${a.icon} isn't there`); - if (a.icon && !/\.(?:svg|png)$/i.test(a.icon)) problems.push("icon: use an .svg or a .png file"); + if (a.icon && !existsSync(a.icon)) problems.push(`logo: ${a.icon} isn't there`); + if (a.icon && !/\.(?:svg|png)$/i.test(a.icon)) problems.push("logo: use an .svg or a .png file (a 1024×1024 PNG works everywhere)"); return problems; } +/** Is this SVG the starter mark create-clip-wallet draws (any accent)? @param {string} text */ +export function isStarterSvg(text) { + return /^<\/svg>\s*$/.test(text); +} + /** @param {string} file @param {(text: string) => string} fn */ function edit(file, fn) { if (!existsSync(file)) return; @@ -109,71 +128,89 @@ export function readIdentity(root) { return existsSync(f) ? JSON.parse(readFileSync(f, "utf8")) : undefined; } +/** The accent colour clip.config.ts sets. @param {string} root */ +export function accentOf(root) { + const f = join(root, CONFIG_FILE); + return existsSync(f) ? /accent:\s*"(#[0-9a-fA-F]{3,6})"/.exec(readFileSync(f, "utf8"))?.[1] : undefined; +} + +/** Is `root` a wallet project (clip.config.ts and wallet.identity.json at its root)? @param {string} root */ +export function isProject(root) { + return existsSync(join(root, CONFIG_FILE)) && existsSync(join(root, IDENTITY_FILE)); +} + /** * Write a new identity into the project at `root`. Validates first and writes nothing on bad answers. * @param {string} root @param {IdentityAnswers} answers - * @returns {{ name: string, rdns: string, slug: string, key: string, extensionId: string, keyFile: string | undefined, listings: string[] }} + * @returns {{ name: string, rdns: string, slug: string, key: string, extensionId: string, keyFile: string | undefined, + * listings: string[], icons: string[], warnings: string[] }} */ export function applyIdentity(root, answers) { const problems = checkIdentity(answers); if (problems.length) throw new Error(`Can't set the wallet's identity:\n${problems.map((p) => ` - ${p}`).join("\n")}`); - const ext = join(root, EXTENSION_DIR); - if (!existsSync(join(ext, "clip.config.ts"))) throw new Error(`${root} isn't a Clip Wallet project (no ${join(EXTENSION_DIR, "clip.config.ts")}).`); + if (!existsSync(join(root, CONFIG_FILE))) throw new Error(`${root} isn't a Clip Wallet project (no ${CONFIG_FILE}).`); const slug = slugOf(answers.name); const previous = readIdentity(root) ?? {}; + const platforms = projectPlatforms(root); - // The extension key: keep the one the project has unless asked for a new one. + // The extension key (it fixes the extension id, also for a wallet that adds its extension later): keep the one the + // project has unless asked for a new one. let key = answers.newKey ? undefined : previous.extension?.key; let keyFile; if (!key) { const { publicKey, privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); key = publicKey.export({ type: "spki", format: "der" }).toString("base64"); keyFile = join(root, KEY_FILE); - mkdirSync(join(ext, ".keys"), { recursive: true, mode: 0o700 }); + mkdirSync(join(root, ".keys"), { recursive: true, mode: 0o700 }); writeFileSync(keyFile, privateKey.export({ type: "pkcs8", format: "pem" }), { mode: 0o600 }); chmodSync(keyFile, 0o600); } - // Icon: the given file, or a starter mark in the accent colour. - const accent = answers.accent ?? /accent:\s*"(#[0-9a-fA-F]{3,6})"/.exec(readFileSync(join(ext, "clip.config.ts"), "utf8"))?.[1] ?? DEFAULT_ACCENT; + // Icons for every platform from one logo, or the starter mark in the accent colour: when a logo is given, when the + // accent changes, and the first time. + const accent = answers.accent ?? accentOf(root) ?? DEFAULT_ACCENT; let icon = previous.icon ?? "./icon.svg"; - mkdirSync(join(ext, "public", "icon"), { recursive: true }); - if (answers.icon) { - const isPng = extname(answers.icon).toLowerCase() === ".png"; - icon = isPng ? "./icon.png" : "./icon.svg"; - copyFileSync(resolve(answers.icon), join(ext, icon)); - // PNG artwork works at every manifest size; for SVG artwork, keep starter PNGs until you export your own. - for (const s of ICON_SIZES) writeFileSync(join(ext, "public", "icon", `${s}.png`), isPng ? readFileSync(resolve(answers.icon)) : iconPng(accent, s)); - } else if (answers.accent || !previous.extension?.key) { - icon = "./icon.svg"; - writeFileSync(join(ext, "icon.svg"), iconSvg(accent)); - for (const s of ICON_SIZES) writeFileSync(join(ext, "public", "icon", `${s}.png`), iconPng(accent, s)); + /** @type {string[]} */ + let icons = []; + /** @type {string[]} */ + let warnings = []; + if (answers.icon || answers.accent || !previous.extension?.key) { + // Re-rendering for a new accent keeps a logo the wallet already has; only the starter mark is redrawn. + const current = previous.icon && existsSync(join(root, previous.icon)) ? join(root, previous.icon) : undefined; + const logo = answers.icon ?? (current && !isStarterSvg(readFileSync(current, "utf8")) ? current : undefined); + const r = renderIcons(root, { logo, accent, platforms }); + icons = r.written; + warnings = r.warnings; + icon = logo ? `./icon${extname(logo).toLowerCase()}` : "./icon.svg"; } - /** @type {{ name: string, description: string, rdns: string, homepage?: string, icon: string, extension: { key: string } }} */ + const appId = answers.appId ?? previous.appId; + /** @type {{ name: string, description: string, rdns: string, homepage?: string, icon: string, appId?: string, extension: { key: string } }} */ const identity = { name: answers.name.trim(), description: answers.description ?? previous.description ?? `${answers.name.trim()}: a non-custodial wallet built on the Clip Wallet kit.`, rdns: answers.rdns, ...((answers.homepage ?? previous.homepage) ? { homepage: answers.homepage ?? previous.homepage } : {}), icon, + ...(appId ? { appId } : {}), extension: { key }, }; writeFileSync(join(root, IDENTITY_FILE), `${JSON.stringify(identity, null, 2)}\n`); - edit(join(ext, "clip.config.ts"), (t) => { + edit(join(root, CONFIG_FILE), (t) => { let out = t; if (answers.accent) out = out.replace(/accent:\s*"#[0-9a-fA-F]{3,6}"/, `accent: ${JSON.stringify(answers.accent)}`); if (answers.networks) out = out.replace(/networks:\s*\[[^\]]*\]/, `networks: [${answers.networks.map((n) => JSON.stringify(n)).join(", ")}]`); + if (answers.languages) out = out.replace(/languages:\s*\[[^\]]*\]/, `languages: [${answers.languages.map((n) => JSON.stringify(n)).join(", ")}]`); return out; }); for (const page of ["popup", "tab"]) { - edit(join(ext, "src", "entrypoints", page, "index.html"), (t) => t.replace(/[^<]*<\/title>/, `${identity.name.replace(/[<&]/g, "")}`)); + edit(join(root, EXTENSION_DIR, "src", "entrypoints", page, "index.html"), (t) => t.replace(/[^<]*<\/title>/, `<title>${identity.name.replace(/[<&]/g, "")}`)); } if (answers.walletConnectProjectId) { - setEnv(join(ext, ".env"), WALLETCONNECT_ENV, answers.walletConnectProjectId); - setEnv(join(root, "packages", "nextjs", ".env.local"), "NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID", answers.walletConnectProjectId); + setEnv(join(root, ".env"), WALLETCONNECT_ENV, answers.walletConnectProjectId); + if (existsSync(join(root, NEXT_DIR))) setEnv(join(root, NEXT_DIR, ".env.local"), "NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID", answers.walletConnectProjectId); } const extensionId = extensionIdFromKey(key); @@ -189,18 +226,28 @@ export function applyIdentity(root, answers) { networks, version: kitVersion(root), }); - return { name: identity.name, rdns: identity.rdns, slug, key, extensionId, keyFile, listings }; + return { name: identity.name, rdns: identity.rdns, slug, key, extensionId, keyFile, listings, icons, warnings }; } /** The networks line of clip.config.ts. @param {string} root */ export function networksOf(root) { - const t = readFileSync(join(root, EXTENSION_DIR, "clip.config.ts"), "utf8"); - const m = /networks:\s*\[([^\]]*)\]/.exec(t); - return m ? [...String(m[1]).matchAll(/"([^"]+)"/g)].map((x) => String(x[1])) : DEFAULT_NETWORKS; + return listOf(root, "networks") ?? DEFAULT_NETWORKS; } -/** The @clip-wallet/extension-kit version the project pins. @param {string} root */ +/** A string-array setting in clip.config.ts (networks, languages). @param {string} root @param {string} field */ +export function listOf(root, field) { + const t = readFileSync(join(root, CONFIG_FILE), "utf8").replace(/^\s*\/\/.*$/gm, ""); + const m = new RegExp(`${field}:\\s*\\[([^\\]]*)\\]`).exec(t); + return m ? [...String(m[1]).matchAll(/"([^"]+)"/g)].map((x) => String(x[1])) : undefined; +} + +/** The @clip-wallet/* version the project pins. @param {string} root */ export function kitVersion(root) { - const pkg = JSON.parse(readFileSync(join(root, EXTENSION_DIR, "package.json"), "utf8")); - return pkg.dependencies?.["@clip-wallet/extension-kit"] ?? "0.0.0"; + for (const f of ["package.json", ...Object.values(PLATFORM_DIRS).map((d) => join(d, "package.json"))]) { + if (!existsSync(join(root, f))) continue; + const pkg = JSON.parse(readFileSync(join(root, f), "utf8")); + const v = Object.entries({ ...pkg.dependencies, ...pkg.devDependencies }).find(([n]) => n.startsWith("@clip-wallet/"))?.[1]; + if (v) return String(v); + } + return "0.0.0"; } diff --git a/packages/create-clip-wallet/src/index.mjs b/packages/create-clip-wallet/src/index.mjs index 934887b..6270d45 100644 --- a/packages/create-clip-wallet/src/index.mjs +++ b/packages/create-clip-wallet/src/index.mjs @@ -1,46 +1,57 @@ -// create-clip-wallet: start your own wallet on the Clip Wallet kit. +// create-clip-wallet: start your own wallet on the Clip Wallet kit, on every platform, from one clip.config.ts. // -// npx create-clip-wallet my-wallet copy the Scaffold-HBAR Clip Wallet template and give it an identity +// npx create-clip-wallet my-wallet a new project: extension, desktop and phone apps (and, with +// --scaffold-hbar, the Scaffold-HBAR dapp), with their own identity // create-clip-wallet identity [--name …] (inside a project) set or change the wallet's identity +// create-clip-wallet brand [--logo …] (inside a project) render every platform's icons from the logo // create-clip-wallet listings (inside a project) regenerate docs/listings drafts // create-clip-wallet mainnet-check (inside a project) what still blocks a mainnet build // -// `npx create-clip-wallet my-wallet` produces the same project as +// `npx create-clip-wallet my-wallet --scaffold-hbar` produces the same project as // `npm create scaffold-hbar@latest -- --template ColdAI-org/scaffold-hbar-clip-wallet` followed by -// `pnpm wallet:identity`: both copy the same template the same way, then run the same identity step. +// `pnpm wallet:identity`: both copy the same template the same way, then run the same identity step. Without +// --scaffold-hbar (or with fewer --platforms) the parts that weren't chosen are removed afterwards. import { existsSync, readFileSync, readdirSync } from "node:fs"; import { basename, dirname, join, resolve } from "node:path"; import { createInterface } from "node:readline/promises"; import { fileURLToPath } from "node:url"; -import { MAINNET_ACKNOWLEDGEMENT, WALLETCONNECT_ENV, isPlaceholderRdns, mainnetProblems, validateConfig, walletKey } from "@clip-wallet/config"; +import { LANGUAGES, MAINNET_ACKNOWLEDGEMENT, WALLETCONNECT_ENV, isPlaceholderRdns, mainnetProblems, platformIds, validateConfig, walletKey } from "@clip-wallet/config"; import { + CHECKLIST_FILE, + CONFIG_FILE, DEFAULT_ACCENT, DEFAULT_NETWORKS, - EXTENSION_DIR, + IDENTITY_FILE, KEY_FILE, + accentOf, applyIdentity, checkIdentity, extensionIdFromKey, + isProject, kitVersion, + listOf, networksOf, parseNetworks, placeholderRdns, + projectPlatforms, readIdentity, slugOf, titleFromDir, } from "./identity.mjs"; +import { PLATFORMS } from "./icons.mjs"; import { writeListings } from "./listings.mjs"; -import { copyTemplate, gitInit, normalizeRootPackageJson, processTemplateManifest, templateDir } from "./template.mjs"; +import { copyTemplate, gitInit, normalizeRootPackageJson, processTemplateManifest, selectParts, templateDir } from "./template.mjs"; -export { applyIdentity, checkIdentity, extensionIdFromKey, parseNetworks, placeholderRdns, slugOf, titleFromDir } from "./identity.mjs"; +export { applyIdentity, checkIdentity, extensionIdFromKey, isStarterSvg, parseNetworks, placeholderRdns, projectPlatforms, slugOf, titleFromDir } from "./identity.mjs"; export { iconPng, iconSvg } from "./icon.mjs"; +export { decodePng, encodeIcns, encodeIco, encodePng, iconPlan, readIcns, readIco, renderIcons } from "./icons.mjs"; export { writeListings } from "./listings.mjs"; -export { copyTemplate, normalizeRootPackageJson, processTemplateManifest, templateDir } from "./template.mjs"; -export { DEFAULT_ACCENT, DEFAULT_NETWORKS }; +export { copyTemplate, normalizeRootPackageJson, processTemplateManifest, pruneScripts, selectParts, stripParts, templateDir } from "./template.mjs"; +export { DEFAULT_ACCENT, DEFAULT_NETWORKS, PLATFORMS }; const PKG = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")); export const VERSION = PKG.version; -export const COMMANDS = ["identity", "listings", "mainnet-check"]; +export const COMMANDS = ["identity", "brand", "listings", "mainnet-check"]; /** @param {string} dir */ function isEmptyDir(dir) { @@ -48,11 +59,21 @@ function isEmptyDir(dir) { } /** - * @typedef {{ command?: string, dir?: string, root?: string, name?: string, rdns?: string, accent?: string, networks?: string[], - * homepage?: string, description?: string, icon?: string, walletConnectProjectId?: string, git: boolean, newKey: boolean, - * yes: boolean, help: boolean, version: boolean }} Args + * @typedef {{ command?: string, dir?: string, root?: string, name?: string, rdns?: string, id?: string, accent?: string, + * networks?: string[], languages?: string[], platforms?: string[], scaffoldHbar?: boolean, homepage?: string, + * description?: string, logo?: string, walletConnectProjectId?: string, git: boolean, newKey: boolean, yes: boolean, + * help: boolean, version: boolean }} Args */ +/** "extension,desktop" -> ["extension", "desktop"], checked. @param {string} text */ +export function parsePlatforms(text) { + const list = [...new Set(parseNetworks(text.toLowerCase()))]; + const bad = list.filter((p) => !(/** @type {readonly string[]} */ (PLATFORMS)).includes(p)); + if (bad.length) throw new Error(`--platforms: ${bad.join(", ")} ${bad.length === 1 ? "isn't a platform" : "aren't platforms"}; use ${PLATFORMS.join(", ")}`); + if (!list.length) throw new Error("--platforms: pick at least one of extension, desktop, mobile"); + return PLATFORMS.filter((p) => list.includes(p)); +} + /** @param {string[]} argv @returns {Args} */ export function parseArgs(argv) { /** @type {Args} */ @@ -71,16 +92,21 @@ export function parseArgs(argv) { else if (a === "--new-key") out.newKey = true; else if (a === "--name") out.name = val(); else if (a === "--rdns") out.rdns = val(); + else if (a === "--id" || a === "--app-id") out.id = val(); else if (a === "--accent") out.accent = val(); else if (a === "--networks") out.networks = parseNetworks(val()); + else if (a === "--languages") out.languages = parseNetworks(val()); + else if (a === "--platforms") out.platforms = parsePlatforms(val()); + else if (a === "--scaffold-hbar") out.scaffoldHbar = true; + else if (a === "--no-scaffold-hbar") out.scaffoldHbar = false; else if (a === "--homepage") out.homepage = val(); else if (a === "--description") out.description = val(); - else if (a === "--icon") out.icon = val(); + else if (a === "--logo" || a === "--icon") out.logo = val(); else if (a === "--walletconnect-project-id") out.walletConnectProjectId = val(); else if (a === "--root") out.root = val(); else if (a === "--mainnet") { throw new Error( - `New wallets start on test networks. Mainnet is a later, deliberate step: complete ${join(EXTENSION_DIR, "MAINNET.md")}, then set mainnet in clip.config.ts (create-clip-wallet mainnet-check tells you what is left).`, + `New wallets start on test networks. Mainnet is a later, deliberate step: complete ${CHECKLIST_FILE}, then set mainnet in clip.config.ts (create-clip-wallet mainnet-check tells you what is left).`, ); } else if (a.startsWith("-")) throw new Error(`Unknown option ${a}. Run with --help.`); else if (!out.command && !out.dir && COMMANDS.includes(a)) out.command = a; @@ -90,29 +116,35 @@ export function parseArgs(argv) { return out; } -export const HELP = `create-clip-wallet ${VERSION}: your own wallet on the Clip Wallet kit (test networks by default) +export const HELP = `create-clip-wallet ${VERSION}: your own wallet on the Clip Wallet kit, on every platform (test networks by default) - npx create-clip-wallet [options] new project: the Scaffold-HBAR Clip Wallet template, with its own identity + npx create-clip-wallet [options] new project: browser extension, desktop app, phone app, one clip.config.ts create-clip-wallet identity [options] in a project: set or change the identity (pnpm wallet:identity) + create-clip-wallet brand [--logo …] in a project: every platform's icons from the logo (pnpm wallet:brand) create-clip-wallet listings in a project: regenerate docs/listings (pnpm wallet:listings) create-clip-wallet mainnet-check in a project: what still blocks a mainnet build (pnpm wallet:mainnet-check) Options - --name "Acme Wallet" the wallet's name (default: from the folder) + --platforms extension,desktop,mobile which apps to make (default: all three) + --scaffold-hbar also the Scaffold-HBAR demo dapp (Next.js, Hedera testnet, /debug page) + --name "Acme Wallet" the wallet's name on every platform (default: from the folder) --rdns com.acme.wallet EIP-6963 id: a reverse domain you own (default: com.example., a placeholder) - --accent #0B7A3B accent colour (white text on it needs 3:1 contrast) + --id com.acme.wallet desktop and phone app id (bundle id / package); default: the rdns + --logo ./logo.png your logo (.png, ideally 1024×1024, or .svg); every icon is rendered from it. + Default: a starter mark in the accent colour + --accent #0B7A3B accent colour (white text on it needs 3:1 contrast); icon and splash background --networks "evm:*,hedera" "evm:*", "evm:", hedera, solana, bitcoin, sui, aptos, cardano, substrate, starknet, ton, near, stellar, tezos, algorand + --languages en,de,ja ${LANGUAGES.join(", ")} (default: all; the first is the fallback) --homepage https://acme.example your wallet's website --description "…" one sentence for the stores (132 characters max) - --icon ./logo.svg your icon (.svg or .png); default: a starter mark in the accent colour --walletconnect-project-id … your WalletConnect Cloud project id (written to .env files, never committed) --new-key identity: replace the extension key (changes the extension id) --no-git don't git init - --root project directory for identity/listings/mainnet-check (default: here) + --root project directory for identity/brand/listings/mainnet-check (default: here) -y, --yes accept defaults, no questions -Mainnet is never switched on here: see packages/extension/MAINNET.md in the project.`; +Mainnet is never switched on here: see MAINNET.md in the project.`; /** * Ask until the answer passes `check` (which returns a problem or undefined). @@ -121,17 +153,27 @@ Mainnet is never switched on here: see packages/extension/MAINNET.md in the proj */ async function ask(rl, log, q, def, check) { for (;;) { - const v = (await rl.question(`${q} (${def}): `)).trim() || def; + const v = (await rl.question(`${q}${def ? ` (${def})` : ""}: `)).trim() || def; const problem = check(v); if (!problem) return v; log(` ${problem}`); } } +/** @param {unknown} fn @returns {string | undefined} */ +function problemOf(fn) { + try { + /** @type {() => void} */ (fn)(); + return undefined; + } catch (e) { + return e instanceof Error ? e.message : String(e); + } +} + /** * Collect identity answers from flags and prompts. * @param {Args} args @param {{ question(q: string): Promise }} rl @param {(s: string) => void} log - * @param {{ name: string, rdns?: string, accent?: string, networks?: string[], askNetworks: boolean }} defaults + * @param {{ name: string, rdns?: string, appId?: string, accent?: string, networks?: string[], languages?: string[], askAll: boolean }} defaults */ async function answersFrom(args, rl, log, defaults) { /** @param {string} field @param {Record} partial */ @@ -145,20 +187,37 @@ async function answersFrom(args, rl, log, defaults) { const name = args.name ?? (prompt ? await ask(rl, log, "Wallet name", defaults.name, (v) => pick("name", { name: v })) : defaults.name); const defRdns = defaults.rdns ?? placeholderRdns(slugOf(name)); const rdns = args.rdns ?? (prompt ? await ask(rl, log, "Reverse domain you own (EIP-6963 rdns)", defRdns, (v) => pick("rdns", { rdns: v })) : defRdns); + const appId = + args.id ?? + (prompt && defaults.askAll + ? await ask(rl, log, "App id for the desktop and phone apps", defaults.appId ?? rdns, (v) => pick("appId", { appId: v })) + : defaults.appId); const accent = args.accent ?? (prompt ? await ask(rl, log, "Accent colour", defaults.accent ?? DEFAULT_ACCENT, (v) => pick("theme", { accent: v })) : defaults.accent); + const logo = + args.logo ?? + (prompt && defaults.askAll + ? (await ask(rl, log, "Logo (.png or .svg; empty = a starter mark)", "", (v) => (v ? pick("logo", { icon: v }) : undefined))) || undefined + : undefined); const networks = args.networks ?? - (prompt && defaults.askNetworks + (prompt && defaults.askAll ? parseNetworks(await ask(rl, log, "Networks", (defaults.networks ?? DEFAULT_NETWORKS).join(", "), (v) => pick("networks", { networks: parseNetworks(v) }))) : defaults.networks); + const languages = + args.languages ?? + (prompt && defaults.askAll + ? parseNetworks(await ask(rl, log, "Languages", (defaults.languages ?? [...LANGUAGES]).join(", "), (v) => pick("languages", { languages: parseNetworks(v) }))) + : defaults.languages); return { name, rdns, + ...(appId && appId !== rdns ? { appId } : {}), ...(accent ? { accent } : {}), ...(networks ? { networks } : {}), + ...(languages ? { languages } : {}), ...(args.homepage ? { homepage: args.homepage } : {}), ...(args.description ? { description: args.description } : {}), - ...(args.icon ? { icon: args.icon } : {}), + ...(logo ? { icon: logo } : {}), ...(args.walletConnectProjectId ? { walletConnectProjectId: args.walletConnectProjectId } : {}), newKey: args.newKey, }; @@ -166,28 +225,90 @@ async function answersFrom(args, rl, log, defaults) { /** @param {ReturnType} r @param {string} root */ function identitySummary(r, root) { + const id = readIdentity(root) ?? {}; + const ids = platformIds(/** @type {never} */ ({ name: r.name, rdns: r.rdns, appId: id.appId, desktop: {}, mobile: {} })); + const platforms = projectPlatforms(root); return [ ` name ${r.name}`, ` rdns ${r.rdns}${isPlaceholderRdns(r.rdns) ? " (placeholder: set one you own before you publish)" : ""}`, - ` extension id ${r.extensionId}`, + platforms.includes("extension") ? ` extension id ${r.extensionId}` : "", + platforms.includes("desktop") ? ` desktop id ${ids.desktop.appId}` : "", + platforms.includes("mobile") ? ` iOS / Android ${ids.ios.bundleIdentifier} / ${ids.android.package}` : "", + platforms.length > 1 ? ` deep links ${ids.scheme}://` : "", r.keyFile ? ` private key ${join(root, KEY_FILE)} (gitignored; back it up, never commit or share it)` : "", + r.icons.length ? ` icons ${r.icons.length} files for ${platforms.join(", ")} (pnpm wallet:brand re-renders them)` : "", ` listings ${r.listings.length - 1} drafts in docs/listings/`, + ...r.warnings.map((w) => ` note ${w}`), ] .filter(Boolean) .join("\n"); } +/** Next steps for the parts a project has. @param {string} dir @param {string} root @param {{ walletConnect: boolean }} o */ +export function nextSteps(dir, root, o) { + const platforms = projectPlatforms(root); + const dapp = existsSync(join(root, "packages", "nextjs")); + return [ + "Next steps:", + ` cd ${dir}`, + " pnpm install", + ...(platforms.includes("extension") + ? [ + "", + " Browser extension (packages/extension)", + " pnpm dev:extension # Chrome with the extension loaded, live reload", + " pnpm extension:build # load packages/extension/.output/chrome-mv3 unpacked in chrome://extensions", + " pnpm extension:zip # the store upload zip", + ] + : []), + ...(platforms.includes("desktop") + ? [ + "", + " Desktop app: macOS, Windows, Linux (packages/desktop)", + " pnpm dev:desktop # run it with live reload", + " pnpm desktop:dist # installers for this computer in packages/desktop/release (unsigned)", + " pnpm desktop:dist:mac # also :win and :linux; signing: docs/signing.md", + ] + : []), + ...(platforms.includes("mobile") + ? [ + "", + " Phone app: iOS, Android (packages/mobile)", + " pnpm --filter mobile start # Metro for a development build", + " pnpm mobile:prebuild # generate ios/ and android/, then pnpm --filter mobile ios (or android)", + " pnpm mobile:export # the JS bundles, no account needed; EAS: pnpm --filter mobile eas:build", + ] + : []), + ...(dapp ? ["", " Scaffold-HBAR dapp (packages/nextjs)", " pnpm next:dev # connect, sign and send on Hedera testnet: http://localhost:3000"] : []), + "", + " pnpm harness # must pass before every commit", + o.walletConnect ? "" : ` set ${WALLETCONNECT_ENV} in .env (copy .env.example; never commit .env)`, + ]; +} + /** Create a project. @param {Args} args @param {{ question(q: string): Promise }} rl @param {(s: string) => void} log */ async function create(args, rl, log) { const dir = args.dir ?? (args.yes ? "my-wallet" : await ask(rl, log, "Folder", "my-wallet", (v) => (isEmptyDir(resolve(v)) ? undefined : "That folder isn't empty."))); const target = resolve(dir); if (!isEmptyDir(target)) throw new Error(`${dir} already exists and isn't empty. Pick a new folder name.`); - const answers = await answersFrom(args, rl, log, { name: titleFromDir(dir), accent: DEFAULT_ACCENT, networks: DEFAULT_NETWORKS, askNetworks: true }); + const platforms = + args.platforms ?? + (args.yes + ? [...PLATFORMS] + : parsePlatforms( + await ask(rl, log, "Platforms (extension, desktop, mobile)", PLATFORMS.join(", "), (v) => problemOf(() => parsePlatforms(v))?.replace(/^--platforms: /, "")), + )); + const scaffoldHbar = + args.scaffoldHbar ?? + (args.yes ? false : /^y(es)?$/i.test(await ask(rl, log, "Add the Scaffold-HBAR demo dapp? (y/N)", "n", (v) => (/^(y|yes|n|no)$/i.test(v) ? undefined : "Answer y or n.")))); + const answers = await answersFrom(args, rl, log, { name: titleFromDir(dir), accent: DEFAULT_ACCENT, networks: DEFAULT_NETWORKS, askAll: true }); const problems = checkIdentity(answers); if (problems.length) throw new Error(`Can't create the wallet:\n${problems.map((p) => ` - ${p}`).join("\n")}`); - // The same steps, in the same order, as create-scaffold-hbar's copyTemplateFiles for this template. + // The same steps, in the same order, as create-scaffold-hbar's copyTemplateFiles for this template; then the parts + // that weren't chosen go. copyTemplate(templateDir(), target); + selectParts(target, { platforms, scaffoldHbar }); normalizeRootPackageJson(target); processTemplateManifest(target, basename(target)); const r = applyIdentity(target, answers); @@ -195,20 +316,14 @@ async function create(args, rl, log) { log( [ "", - `Created ${r.name} in ${dir}.`, + `Created ${r.name} in ${dir}: ${[...platforms, ...(scaffoldHbar ? ["Scaffold-HBAR dapp"] : [])].join(", ")}, all from ${CONFIG_FILE}.`, "", identitySummary(r, target), "", - "Next steps:", - ` cd ${dir}`, - " pnpm install", - " pnpm extension:build # load packages/extension/.output/chrome-mv3 unpacked in Chrome", - " pnpm next:dev # the demo dapp and Scaffold-HBAR debug page: http://localhost:3000", - " pnpm harness # must pass before every commit", - answers.walletConnectProjectId ? "" : ` set ${WALLETCONNECT_ENV} in packages/extension/.env (copy .env.example; never commit .env)`, + ...nextSteps(dir, target, { walletConnect: !!answers.walletConnectProjectId }), "", - "Your wallet runs on test networks. Mainnet stays off until packages/extension/MAINNET.md is complete and", - "clip.config.ts has mainnet: { enabled: true, acknowledged: MAINNET_ACKNOWLEDGEMENT }.", + "Your wallet runs on test networks. Mainnet stays off until MAINNET.md is complete and clip.config.ts has", + "mainnet: { enabled: true, acknowledged: MAINNET_ACKNOWLEDGEMENT }. Store accounts and code signing: docs/signing.md.", git ? "" : "(git isn't set up here: run git init yourself.)", ] .filter((l, i, a) => l !== "" || a[i - 1] !== "") @@ -220,8 +335,8 @@ async function create(args, rl, log) { /** @param {string} [root] */ function projectRoot(root) { const r = resolve(root ?? "."); - if (!existsSync(join(r, EXTENSION_DIR, "clip.config.ts"))) { - throw new Error(`${r} isn't a Clip Wallet project: run this in the folder create-clip-wallet or create-scaffold-hbar made (or pass --root).`); + if (!isProject(r)) { + throw new Error(`${r} isn't a Clip Wallet project (no ${CONFIG_FILE} and ${IDENTITY_FILE}): run this in the folder create-clip-wallet or create-scaffold-hbar made (or pass --root).`); } return r; } @@ -230,17 +345,32 @@ function projectRoot(root) { async function identity(args, rl, log) { const root = projectRoot(args.root); const current = readIdentity(root) ?? {}; - const accent = /accent:\s*"(#[0-9a-fA-F]{3,6})"/.exec(readFileSync(join(root, EXTENSION_DIR, "clip.config.ts"), "utf8"))?.[1]; + const accent = accentOf(root); const answers = await answersFrom(args, rl, log, { name: current.name && current.name !== "My Wallet" ? current.name : titleFromDir(root), rdns: current.rdns && !isPlaceholderRdns(current.rdns) ? current.rdns : undefined, + appId: current.appId, ...(accent ? { accent } : {}), - askNetworks: false, + askAll: false, }); - // Keep the current accent unless one was asked for (a new accent also redraws the starter icon). + // Keep the current accent unless one was asked for (a new accent also re-renders the icons). if (!args.accent && answers.accent === accent) delete answers.accent; const r = applyIdentity(root, answers); - log([`Identity set for ${r.name}:`, identitySummary(r, root), "", "Run pnpm install (if you haven't), then pnpm extension:build and pnpm harness."].join("\n")); + log([`Identity set for ${r.name}:`, identitySummary(r, root), "", "Run pnpm install (if you haven't), then pnpm harness and build the apps."].join("\n")); + return 0; +} + +/** Every platform's icons from the logo (and the accent). @param {Args} args @param {(s: string) => void} log */ +function brand(args, log) { + const root = projectRoot(args.root); + const current = readIdentity(root) ?? {}; + const r = applyIdentity(root, { + name: current.name, + rdns: current.rdns, + accent: args.accent ?? accentOf(root) ?? DEFAULT_ACCENT, + ...(args.logo ? { icon: args.logo } : {}), + }); + log([`Rendered ${r.icons.length} icon files for ${projectPlatforms(root).join(", ")}:`, ...r.icons.map((f) => ` ${f}`), ...r.warnings.map((w) => `note: ${w}`)].join("\n")); return 0; } @@ -266,13 +396,13 @@ function listings(args, log) { /** * What still blocks a mainnet build: the clip.config checks (mainnetProblems), the acknowledgement, and every box in - * packages/extension/MAINNET.md. @param {string} root + * MAINNET.md. @param {string} root */ export function mainnetCheck(root) { const id = readIdentity(root) ?? {}; /** @type {Record} */ const env = {}; - const envFile = join(root, EXTENSION_DIR, ".env"); + const envFile = join(root, ".env"); if (existsSync(envFile)) { for (const line of readFileSync(envFile, "utf8").split(/\r?\n/)) { const m = /^\s*(CLIP_[A-Z0-9_]+)\s*=\s*(.+?)\s*$/.exec(line); @@ -280,18 +410,19 @@ export function mainnetCheck(root) { } } Object.assign(env, process.env); - const r = validateConfig({ ...id, networks: networksOf(root) }); + const languages = listOf(root, "languages"); + const r = validateConfig({ ...id, networks: networksOf(root), ...(languages ? { languages } : {}) }); const problems = r.ok ? mainnetProblems(r.config, env) : r.problems; // Comments stripped: clip.config.ts explains the mainnet object in a comment. - const config = readFileSync(join(root, EXTENSION_DIR, "clip.config.ts"), "utf8") + const config = readFileSync(join(root, CONFIG_FILE), "utf8") .replace(/\/\*[\s\S]*?\*\//g, "") .replace(/(^|[^:])\/\/.*$/gm, "$1"); const enabled = /\bmainnet\s*:\s*\{/.test(config); if (!/\backnowledged\s*:\s*MAINNET_ACKNOWLEDGEMENT\b/.test(config) && !config.includes(MAINNET_ACKNOWLEDGEMENT)) { problems.push("clip.config.ts: mainnet needs { enabled: true, acknowledged: MAINNET_ACKNOWLEDGEMENT } once the checklist is done"); } - const checklist = join(root, EXTENSION_DIR, "MAINNET.md"); - if (!existsSync(checklist)) problems.push(`${join(EXTENSION_DIR, "MAINNET.md")} is missing: restore it from the template`); + const checklist = join(root, CHECKLIST_FILE); + if (!existsSync(checklist)) problems.push(`${CHECKLIST_FILE} is missing: restore it from the template`); else { for (const line of readFileSync(checklist, "utf8").split(/\r?\n/)) { const m = /^\s*- \[ \] (.+)$/.exec(line); @@ -330,11 +461,12 @@ export async function main(argv, io = {}) { log(VERSION); return 0; } + if (args.command === "brand") return brand(args, log); + if (args.command === "listings") return listings(args, log); + if (args.command === "mainnet-check") return mainnet(args, log); const rl = io.question ? { question: io.question, close() {} } : createInterface({ input: process.stdin, output: process.stdout }); try { if (args.command === "identity") return await identity(args, rl, log); - if (args.command === "listings") return listings(args, log); - if (args.command === "mainnet-check") return mainnet(args, log); return await create(args, rl, log); } finally { rl.close(); diff --git a/packages/create-clip-wallet/src/listings.mjs b/packages/create-clip-wallet/src/listings.mjs index 9736dce..e8aa4a9 100644 --- a/packages/create-clip-wallet/src/listings.mjs +++ b/packages/create-clip-wallet/src/listings.mjs @@ -22,12 +22,12 @@ const has = (id, family) => id.networks.some((n) => n === family || n.startsWith const PREREQS = (/** @type {ListingIdentity} */ id) => `## Before you submit anything - [ ] The extension is published on the Chrome Web Store under id \`${id.extensionId}\` (fixed by \`extension.key\` in - packages/extension/wallet.identity.json; keep packages/extension/.keys/extension.pem safe and out of git). + wallet.identity.json; keep .keys/extension.pem safe and out of git). - [ ] \`${site(id)}\` is live and says who runs ${id.name}, how recovery works (the phrase never leaves the device) and how to get support. ${id.homepage ? "" : "Set `homepage` in wallet.identity.json: this placeholder is derived from the rdns."} - [ ] You own the domain behind \`${id.rdns}\`. - [ ] Icons are hosted at stable https URLs you control (PNG, square). -- [ ] Registries list production wallets: finish packages/extension/MAINNET.md first.`; +- [ ] Registries list production wallets: finish MAINNET.md first.`; /** @param {ListingIdentity} id */ function eip6963(id) { @@ -43,7 +43,7 @@ ${id.name} announces, and that nobody else announces the same rdns. | --- | --- | --- | | \`info.name\` | ${id.name} | wallet.identity.json \`name\` | | \`info.rdns\` | \`${id.rdns}\` | wallet.identity.json \`rdns\` (a reverse domain you own) | -| \`info.icon\` | data:image/svg+xml;base64,… | packages/extension/icon.svg, inlined at build time | +| \`info.icon\` | data:image/svg+xml;base64,… | icon.svg (next to clip.config.ts), inlined at build time | | \`info.uuid\` | a fresh UUIDv4 per page load | 1Mask | | Chrome extension id | \`${id.extensionId}\` | wallet.identity.json \`extension.key\` | @@ -96,7 +96,7 @@ ${DRAFT(id.name)} Wallets are listed in the WalletConnect Explorer (the wallet list AppKit and WalletConnect-powered dapps show) through the Reown dashboard: cloud.reown.com, your wallet project, "Get listed". Reown reviews the submission and tests the -wallet. ${id.name}'s WalletConnect side uses its own project id (CLIP_WALLETCONNECT_PROJECT_ID in packages/extension/.env, +wallet. ${id.name}'s WalletConnect side uses its own project id (CLIP_WALLETCONNECT_PROJECT_ID in .env, never committed); the Explorer listing belongs to that same project. ## Draft entry diff --git a/packages/create-clip-wallet/src/template.mjs b/packages/create-clip-wallet/src/template.mjs index cd46ce8..097609a 100644 --- a/packages/create-clip-wallet/src/template.mjs +++ b/packages/create-clip-wallet/src/template.mjs @@ -4,7 +4,7 @@ // template.json, then `git init -b main` and a first commit. See create-scaffold-hbar 0.4.x copyTemplateFiles / // processTemplateManifest. import { execFileSync } from "node:child_process"; -import { cpSync, existsSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs"; +import { cpSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs"; import { basename, dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -21,7 +21,7 @@ export function templateDir() { throw new Error("The wallet template isn't in this copy of create-clip-wallet. Reinstall it (npx create-clip-wallet@latest)."); } -const SKIP = new Set([".git", "node_modules", ".next", ".env", ".output", ".output-fixtures", ".wxt", ".keys"]); +const SKIP = new Set([".git", "node_modules", ".next", ".env", ".output", ".output-fixtures", ".wxt", ".keys", ".expo"]); /** @param {string} src */ function shouldCopy(src) { @@ -120,3 +120,100 @@ export function gitInit(dir, message) { return false; } } + +/* ------------------------------------------------------------------ platforms */ + +/** What a project can contain besides the wallet's config: the three platforms and the Scaffold-HBAR dapp. */ +export const PARTS = /** @type {const} */ (["extension", "desktop", "mobile", "nextjs"]); + +/** + * Blocks of text that belong to one part, removed when that part isn't in the project. Markers sit on lines of their + * own and stay when the part is kept (so a project with everything is byte-for-byte the template): + * … Markdown + * # platform:desktop … # /platform:desktop YAML, shell + * // platform:desktop … // /platform:desktop JavaScript + * | a table row | … | one line (Markdown tables can't hold block markers) + * Part names: extension, desktop, mobile, nextjs (the Scaffold-HBAR dapp). + * @param {string} text @param {ReadonlySet} keep + */ +export function stripParts(text, keep) { + const lines = text.split("\n"); + const out = []; + /** @type {string[]} */ + const stack = []; + for (const line of lines) { + const open = /^\s*(?:)?\s*$/.exec(line); + const close = /^\s*(?:)?\s*$/.exec(line); + const dropping = stack.some((p) => !keep.has(p)); + if (open) { + stack.push(/** @type {string} */ (open[1])); + if (!dropping && keep.has(/** @type {string} */ (open[1]))) out.push(line); + continue; + } + if (close) { + const p = stack.pop(); + if (p === undefined || p !== close[1]) throw new Error(`mismatched platform markers: ${p ?? "none"} closed by ${close[1]}`); + if (!stack.some((x) => !keep.has(x)) && keep.has(p)) out.push(line); + continue; + } + const only = //.exec(line); + if (!dropping && (!only || keep.has(/** @type {string} */ (only[1])))) out.push(line); + } + if (stack.length) throw new Error(`unclosed platform marker: ${stack.join(", ")}`); + return out.join("\n"); +} + +/** + * Root package.json scripts without the parts that aren't there: scripts that run a removed package go, and so do + * steps of composite scripts ("pnpm a && pnpm b") that call a removed script. @param {Record} scripts + * @param {readonly string[]} removed package folders (extension, desktop, mobile, nextjs) + */ +export function pruneScripts(scripts, removed) { + const out = { ...scripts }; + const gone = new Set(); + for (const [k, v] of Object.entries(out)) { + if (removed.some((p) => v.includes(`--filter @sh/${p}`))) { + delete out[k]; + gone.add(k); + } + } + for (let changed = true; changed; ) { + changed = false; + for (const [k, v] of Object.entries(out)) { + const steps = v.split(" && "); + const kept = steps.filter((s) => !gone.has(s.trim().replace(/^pnpm\s+/, "").split(/\s+/)[0] ?? "")); + if (kept.length === steps.length) continue; + changed = true; + if (kept.length) out[k] = kept.join(" && "); + else { + delete out[k]; + gone.add(k); + } + } + } + return out; +} + +/** Files that carry platform markers. */ +export const MARKED_FILES = ["README.md", "AGENTS.md", "llms.txt", "MAINNET.md", "docs/signing.md", ".github/workflows/ci.yaml", "tools/verify-provenance.mjs"]; + +/** + * Keep only the chosen platforms (and the Scaffold-HBAR dapp when asked): remove the other packages, their scripts and + * their sections of the docs. Everything chosen = the template unchanged. @param {string} projectDir + * @param {{ platforms: readonly string[], scaffoldHbar: boolean }} o + */ +export function selectParts(projectDir, o) { + const keep = new Set([...o.platforms, ...(o.scaffoldHbar ? ["nextjs"] : [])]); + const removed = PARTS.filter((p) => !keep.has(p)); + for (const p of removed) rmSync(join(projectDir, "packages", p), { recursive: true, force: true }); + if (!removed.length) return removed; + const pkgFile = join(projectDir, "package.json"); + const pkg = JSON.parse(readFileSync(pkgFile, "utf8")); + pkg.scripts = pruneScripts(pkg.scripts ?? {}, removed); + writeFileSync(pkgFile, `${JSON.stringify(pkg, null, 2)}\n`); + for (const f of MARKED_FILES) { + const file = join(projectDir, f); + if (existsSync(file)) writeFileSync(file, stripParts(readFileSync(file, "utf8"), keep)); + } + return removed; +} diff --git a/packages/create-clip-wallet/test/icons.test.ts b/packages/create-clip-wallet/test/icons.test.ts new file mode 100644 index 0000000..8126779 --- /dev/null +++ b/packages/create-clip-wallet/test/icons.test.ts @@ -0,0 +1,197 @@ +import { describe, expect, it } from "vitest"; +import { platformIds, defineConfig } from "@clip-wallet/config"; +import { + canvas, + decodePng, + encodeIcns, + encodeIco, + encodePng, + fit, + iconPlan, + isOpaque, + readIcns, + readIco, + resize, + silhouette, + starterRasters, +} from "../src/icons.mjs"; +import { pruneScripts, stripParts } from "../src/template.mjs"; +import { deflateSync } from "node:zlib"; + +/** A minimal PNG from raw scanlines (each with its filter byte), for decoder tests. */ +function pngOf(w: number, h: number, depth: number, type: number, rows: Buffer[], extra: Record = {}): Buffer { + const table = Array.from({ length: 256 }, (_, n) => { + let c = n; + for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + return c >>> 0; + }); + const crc = (b: Buffer) => { + let c = 0xffffffff; + for (const x of b) c = table[(c ^ x) & 0xff]! ^ (c >>> 8); + return (c ^ 0xffffffff) >>> 0; + }; + const chunk = (t: string, d: Buffer) => { + const len = Buffer.alloc(4); + len.writeUInt32BE(d.length); + const body = Buffer.concat([Buffer.from(t), d]); + const c = Buffer.alloc(4); + c.writeUInt32BE(crc(body)); + return Buffer.concat([len, body, c]); + }; + const ihdr = Buffer.alloc(13); + ihdr.writeUInt32BE(w, 0); + ihdr.writeUInt32BE(h, 4); + ihdr[8] = depth; + ihdr[9] = type; + return Buffer.concat([ + Buffer.from("89504e470d0a1a0a", "hex"), + chunk("IHDR", ihdr), + ...Object.entries(extra).map(([t, d]) => chunk(t, d)), + chunk("IDAT", deflateSync(Buffer.concat(rows))), + chunk("IEND", Buffer.alloc(0)), + ]); +} + +describe("iconPlan: one logo → every platform's icon files", () => { + it("lists exactly the files each platform needs", () => { + const by = (platforms: ("extension" | "desktop" | "mobile")[]) => iconPlan(platforms).map((p) => p.file); + expect(by(["extension"])).toEqual(["icon.svg", ...[16, 32, 48, 128].map((s) => `packages/extension/public/icon/${s}.png`)]); + const desktop = by(["desktop"]); + expect(desktop).toContain("packages/desktop/build/icon.icns"); + expect(desktop).toContain("packages/desktop/build/icon.ico"); + expect(desktop).toContain("packages/desktop/build/icons/1024x1024.png"); + expect(desktop.filter((f) => f.includes("/tray/"))).toEqual(["tray.png", "tray@2x.png", "trayTemplate.png", "trayTemplate@2x.png"].map((f) => `packages/desktop/src/renderer/public/tray/${f}`)); + expect(by(["mobile"]).slice(1)).toEqual(["icon", "adaptive-icon", "adaptive-monochrome", "splash-icon"].map((f) => `packages/mobile/assets/${f}.png`)); + expect(iconPlan(["mobile"], { logoExt: ".png" })[0]!.file).toBe("icon.png"); + expect(iconPlan(["extension", "desktop", "mobile"])).toHaveLength(24); + }); + + it("the icon files are the ones the platform configs point at", async () => { + const { DESKTOP_ICONS } = await import("../../desktop-kit/src/builder"); + const { MOBILE_ASSETS } = await import("../../mobile-kit/src/expo"); + const files = iconPlan(["desktop", "mobile"]).map((p) => p.file); + expect(files).toContain(`packages/desktop/${DESKTOP_ICONS.mac}`); + expect(files).toContain(`packages/desktop/${DESKTOP_ICONS.win}`); + expect(files).toContain(`packages/desktop/${DESKTOP_ICONS.linux}/512x512.png`); + for (const f of Object.values(MOBILE_ASSETS)) expect(files).toContain(`packages/mobile/${f.replace(/^\.\//, "")}`); + }); +}); + +describe("PNG, ICO and ICNS with Node alone", () => { + it("round-trips RGBA and decodes grey, RGB and palette PNGs", () => { + const img = canvas(3, 2, [10, 20, 30, 128]); + img.data.set([255, 0, 0, 255], 4); + const back = decodePng(encodePng(img)); + expect(back).toMatchObject({ width: 3, height: 2 }); + expect([...back.data]).toEqual([...img.data]); + // A 2×1 palette PNG (bit depth 1) with a transparent first entry, as image editors write them. + const pal = pngOf(2, 1, 1, 3, [Buffer.from([0, 0b01000000])], { PLTE: Buffer.from([0, 0, 0, 255, 0, 0]), tRNS: Buffer.from([0]) }); + expect([...decodePng(pal).data]).toEqual([0, 0, 0, 0, 255, 0, 0, 255]); + // 8-bit grey with alpha, and 16-bit RGB. + expect([...decodePng(pngOf(1, 1, 8, 4, [Buffer.from([0, 200, 100])])).data]).toEqual([200, 200, 200, 100]); + expect([...decodePng(pngOf(1, 1, 16, 2, [Buffer.from([0, 1, 2, 3, 4, 5, 6])])).data]).toEqual([1, 3, 5, 255]); + expect(() => decodePng(Buffer.from("not a png"))).toThrow(/not a PNG/); + }); + + it("writes Windows .ico and macOS .icns containers with PNG entries", () => { + const png = (s: number) => encodePng(canvas(s, s, [1, 2, 3, 255])); + const ico = encodeIco([16, 256].map((s) => ({ size: s, png: png(s) }))); + expect(ico.readUInt16LE(2)).toBe(1); + expect(readIco(ico).map((e) => [e.size, decodePng(e.png).width])).toEqual([ + [16, 16], + [256, 256], + ]); + const icns = encodeIcns([ + { type: "ic07", png: png(128) }, + { type: "ic10", png: png(1024) }, + ]); + expect(icns.toString("ascii", 0, 4)).toBe("icns"); + expect(icns.readUInt32BE(4)).toBe(icns.length); + expect(readIcns(icns).map((e) => [e.type, decodePng(e.png).width])).toEqual([ + ["ic07", 128], + ["ic10", 1024], + ]); + }); +}); + +describe("raster operations", () => { + it("shrinks by area average with premultiplied alpha (no dark fringes)", () => { + const src = canvas(4, 1); + src.data.set([255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0]); + const half = resize(src, 2, 1); + expect([...half.data]).toEqual([255, 255, 255, 255, 0, 0, 0, 0]); + const one = resize(src, 1, 1); + expect([...one.data]).toEqual([255, 255, 255, 128]); + }); + + it("fits a logo into a box on a background, keeping its aspect ratio", () => { + const logo = canvas(20, 10, [255, 0, 0, 255]); + const out = fit(logo, 40, 20, "#0000FF"); + expect([...out.data.subarray(0, 4)]).toEqual([0, 0, 255, 255]); + expect([...out.data.subarray((20 * 40 + 20) * 4, (20 * 40 + 20) * 4 + 4)]).toEqual([255, 0, 0, 255]); + expect(isOpaque(out)).toBe(true); + expect(isOpaque(fit(logo, 40, 20))).toBe(false); + }); + + it("makes a one-colour silhouette from alpha, or from contrast with the background for opaque art", () => { + const transparent = canvas(2, 1); + transparent.data.set([9, 9, 9, 255, 9, 9, 9, 0]); + expect([...silhouette(transparent, "#FFFFFF").data]).toEqual([255, 255, 255, 255, 255, 255, 255, 0]); + const opaque = canvas(200, 1, [0, 0, 128, 255]); + opaque.data.set([255, 255, 255, 255], 100 * 4); + const s = silhouette(opaque, "#000000"); + expect(s.data[3]).toBe(0); + expect(s.data[100 * 4 + 3]).toBe(255); + }); + + it("draws the starter mark: the whole mark and the white glyph", () => { + const { full, glyph } = starterRasters("#4F46E5", 128); + expect([...full.data.subarray((64 * 128 + 5) * 4, (64 * 128 + 5) * 4 + 4)]).toEqual([0x4f, 0x46, 0xe5, 255]); + expect(full.data[3]).toBe(0); + expect([...glyph.data.subarray((64 * 128 + 64) * 4, (64 * 128 + 64) * 4 + 4)]).toEqual([255, 255, 255, 255]); + expect(glyph.data[(64 * 128 + 5) * 4 + 3]).toBe(0); + }); +}); + +describe("config → platform mappers used by the project", () => { + it("ids for every platform come from clip.config", () => { + const ids = platformIds(defineConfig({ name: "Acme Wallet", rdns: "com.acme.wallet", appId: "com.example.mywallet" })); + expect([ids.desktop.appId, ids.ios.bundleIdentifier, ids.android.package, ids.scheme]).toEqual(["com.example.mywallet.desktop", "com.example.mywallet", "com.example.mywallet", "acmewallet"]); + }); +}); + +describe("parts of a project", () => { + it("strips the sections of parts that aren't there and keeps everything else byte for byte", () => { + const text = [ + "a", + "", + "desktop text", + "", + "# platform:mobile", + "mobile: yes", + "# /platform:mobile", + "| row | desktop only |", + "// platform:nextjs", + "dapp();", + "// /platform:nextjs", + "z", + ].join("\n"); + expect(stripParts(text, new Set(["desktop", "mobile", "nextjs"]))).toBe(text); + expect(stripParts(text, new Set(["mobile"]))).toBe(["a", "# platform:mobile", "mobile: yes", "# /platform:mobile", "z"].join("\n")); + expect(() => stripParts("\nx", new Set())).toThrow(/unclosed/); + }); + + it("prunes root scripts of removed packages, including steps of composite scripts", () => { + const scripts = { + build: "pnpm extension:build && pnpm desktop:build && pnpm next:build", + "extension:build": "pnpm --filter @sh/extension build", + "desktop:build": "pnpm --filter @sh/desktop build", + "next:build": "pnpm --filter @sh/nextjs build", + "next:lint": "pnpm --filter @sh/nextjs lint", + lint: "pnpm next:lint", + harness: "node tools/harness/check.mjs", + }; + expect(pruneScripts(scripts, ["extension", "nextjs"])).toEqual({ build: "pnpm desktop:build", "desktop:build": "pnpm --filter @sh/desktop build", harness: "node tools/harness/check.mjs" }); + expect(pruneScripts(scripts, ["extension", "desktop", "nextjs"])).toEqual({ harness: "node tools/harness/check.mjs" }); + }); +}); diff --git a/packages/create-clip-wallet/test/scaffold.test.ts b/packages/create-clip-wallet/test/scaffold.test.ts index 9db885a..270e03c 100644 --- a/packages/create-clip-wallet/test/scaffold.test.ts +++ b/packages/create-clip-wallet/test/scaffold.test.ts @@ -2,10 +2,12 @@ import { execFileSync } from "node:child_process"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, relative } from "node:path"; +import { deflateSync } from "node:zlib"; import { afterEach, describe, expect, it } from "vitest"; import { applyIdentity, copyTemplate, + decodePng, extensionIdFromKey, iconPng, main, @@ -13,6 +15,8 @@ import { normalizeRootPackageJson, parseArgs, processTemplateManifest, + readIcns, + readIco, templateDir, } from "../src/index.mjs"; import { restoreDotfiles } from "../src/template.mjs"; @@ -29,6 +33,10 @@ afterEach(() => { const quiet = { log: () => {}, question: async () => "" }; const read = (f: string) => readFileSync(f, "utf8"); const json = (f: string) => JSON.parse(read(f)); +const size = (f: string) => { + const img = decodePng(readFileSync(f)); + return [img.width, img.height]; +}; /** Every file under dir (relative), without .git and the private key. */ function tree(dir: string): string[] { @@ -48,42 +56,63 @@ function tree(dir: string): string[] { const FLAGS = ["--name", "Acme Wallet", "--rdns", "com.acme.wallet", "--accent", "#0B7A3B", "--networks", "evm:*,hedera,ton,near", "--yes"]; describe("create-clip-wallet ", () => { - it("copies the Scaffold-HBAR template and gives the wallet its own identity", async () => { + it("makes the extension, the desktop app and the phone app from one clip.config.ts, with the wallet's own identity", async () => { const target = join(tmp(), "acme-wallet"); const out: string[] = []; - expect(await main([target, ...FLAGS, "--walletconnect-project-id", "a".repeat(32)], { ...quiet, log: (s) => out.push(s) })).toBe(0); + expect(await main([target, ...FLAGS, "--id", "com.acme.app", "--languages", "en,de,ja", "--walletconnect-project-id", "a".repeat(32)], { ...quiet, log: (s) => out.push(s) })).toBe(0); // The create-scaffold-hbar copy step: template.json consumed, rename map applied. expect(existsSync(join(target, "template.json"))).toBe(false); expect(json(join(target, "package.json")).name).toBe("acme-wallet"); - for (const f of ["AGENTS.md", "llms.txt", ".harness/spec.yaml", ".github/workflows/ci.yaml", ".github/workflows/fresh-scaffold.yaml", "packages/nextjs/app/debug/page.tsx", "packages/extension/MAINNET.md", "tools/harness/check.mjs"]) { + for (const f of ["AGENTS.md", "llms.txt", "clip.config.ts", "wallet.identity.json", "MAINNET.md", "docs/signing.md", "tools/harness/check.mjs"]) { expect(existsSync(join(target, f)), f).toBe(true); } + // Every platform; no Scaffold-HBAR dapp unless asked. + expect(readdirSync(join(target, "packages")).sort()).toEqual(["desktop", "extension", "mobile"]); + const scripts = json(join(target, "package.json")).scripts; + expect(Object.keys(scripts).filter((k) => /next|lint|format/.test(k))).toEqual([]); + expect(scripts.build).toBe("pnpm extension:build && pnpm desktop:build"); + expect(scripts["check-types"]).toBe("pnpm extension:check-types && pnpm desktop:check-types && pnpm mobile:check-types"); + expect(scripts).toMatchObject({ "dev:extension": "pnpm --filter @sh/extension dev", "dev:desktop": "pnpm --filter @sh/desktop dev", "dev:mobile": "pnpm --filter @sh/mobile start" }); + const readme = read(join(target, "README.md")); + expect(readme).not.toMatch(/packages\/nextjs|platform:nextjs|only:nextjs/); + expect(readme).toMatch(/platform:desktop/); + expect(read(join(target, "AGENTS.md"))).not.toMatch(/### Change the dapp/); - const id = json(join(target, "packages/extension/wallet.identity.json")); - expect(id).toMatchObject({ name: "Acme Wallet", rdns: "com.acme.wallet", icon: "./icon.svg" }); + const id = json(join(target, "wallet.identity.json")); + expect(id).toMatchObject({ name: "Acme Wallet", rdns: "com.acme.wallet", appId: "com.acme.app", icon: "./icon.svg" }); expect(id.extension.key).toMatch(/^[A-Za-z0-9+/]{300,}={0,2}$/); const extId = extensionIdFromKey(id.extension.key); expect(extId).toMatch(/^[a-p]{32}$/); - // The private key: next to the extension, 0600, never in git, never in the identity file. - const pem = join(target, "packages/extension/.keys/extension.pem"); + // The private key: at the root, 0600, never in git, never in the identity file. + const pem = join(target, ".keys/extension.pem"); expect(read(pem)).toMatch(/^-----BEGIN PRIVATE KEY-----/); expect(statSync(pem).mode & 0o077).toBe(0); - expect(read(join(target, "packages/extension/wallet.identity.json"))).not.toMatch(/PRIVATE/); + expect(read(join(target, "wallet.identity.json"))).not.toMatch(/PRIVATE/); const tracked = execFileSync("git", ["ls-files"], { cwd: target }).toString(); - expect(tracked).toMatch(/packages\/extension\/wallet\.identity\.json/); + expect(tracked).toMatch(/^wallet\.identity\.json$/m); expect(tracked).not.toMatch(/\.pem|\.keys|\.env\b(?!\.example)/); - const config = read(join(target, "packages/extension/clip.config.ts")); + const config = read(join(target, "clip.config.ts")); expect(config).toContain('accent: "#0B7A3B"'); expect(config).toContain('networks: ["evm:*", "hedera", "ton", "near"]'); + expect(config).toContain('languages: ["en", "de", "ja"]'); expect(config).toContain("mainnet: false,"); - expect(read(join(target, "packages/extension/icon.svg"))).toContain("#0B7A3B"); - expect(read(join(target, "packages/extension/public/icon/128.png")).length).toBeGreaterThan(100); + expect(read(join(target, "icon.svg"))).toContain("#0B7A3B"); expect(read(join(target, "packages/extension/src/entrypoints/popup/index.html"))).toContain("Acme Wallet"); - expect(read(join(target, "packages/extension/.env"))).toBe(`CLIP_WALLETCONNECT_PROJECT_ID=${"a".repeat(32)}\n`); - expect(read(join(target, "packages/nextjs/.env.local"))).toBe(`NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=${"a".repeat(32)}\n`); + expect(read(join(target, ".env"))).toBe(`CLIP_WALLETCONNECT_PROJECT_ID=${"a".repeat(32)}\n`); + + // Icons for every platform, rendered from the one logo. + for (const s of [16, 32, 48, 128]) expect(size(join(target, `packages/extension/public/icon/${s}.png`))).toEqual([s, s]); + expect(readIcns(readFileSync(join(target, "packages/desktop/build/icon.icns"))).map((e) => decodePng(e.png).width)).toEqual([16, 32, 128, 256, 512, 1024, 32, 64, 256, 512]); + expect(readIco(readFileSync(join(target, "packages/desktop/build/icon.ico"))).map((e) => e.size)).toEqual([16, 24, 32, 48, 64, 128, 256]); + for (const s of [16, 32, 48, 64, 128, 256, 512, 1024]) expect(size(join(target, `packages/desktop/build/icons/${s}x${s}.png`))).toEqual([s, s]); + expect(size(join(target, "packages/desktop/src/renderer/public/tray/trayTemplate@2x.png"))).toEqual([32, 32]); + for (const f of ["icon", "adaptive-icon", "adaptive-monochrome", "splash-icon"]) expect(size(join(target, `packages/mobile/assets/${f}.png`))).toEqual([1024, 1024]); + // iOS icons can't be transparent: the starter's mark sits on the accent colour, corner to corner. + const ios = decodePng(readFileSync(join(target, "packages/mobile/assets/icon.png"))); + expect([...ios.data.subarray(0, 4)]).toEqual([0x0b, 0x7a, 0x3b, 255]); // Listing drafts for this identity, only for the families it turns on. const listings = readdirSync(join(target, "docs/listings")).sort(); @@ -91,19 +120,41 @@ describe("create-clip-wallet ", () => { expect(read(join(target, "docs/listings/ton-connect.md"))).toContain('"app_name": "acmewallet"'); expect(read(join(target, "docs/listings/walletconnect-explorer.md"))).toContain('"injected_id": "com.acme.wallet"'); expect(read(join(target, "docs/listings/README.md"))).toContain(extId); - expect(read(join(target, "docs/listings/README.md"))).toMatch(/Stellar Wallets Kit \| not drafted: Stellar is off/); const text = out.join("\n"); expect(text).toContain(extId); - expect(text).toMatch(/pnpm install/); + expect(text).toContain("com.acme.app.desktop"); + expect(text).toContain("com.acme.app / com.acme.app"); + expect(text).toMatch(/pnpm dev:desktop/); + expect(text).toMatch(/pnpm --filter mobile start/); + expect(text).toMatch(/pnpm extension:zip/); expect(text).toMatch(/test networks/); + expect(text).not.toMatch(/next:dev/); expect(text).not.toMatch(/PRIVATE KEY/); }); - it("produces the same project as create-scaffold-hbar's copy step followed by pnpm wallet:identity", async () => { + it("--platforms and --scaffold-hbar choose the parts; scripts and docs follow", async () => { + const target = join(tmp(), "phone-wallet"); + expect(await main([target, "--platforms", "mobile,desktop", "--scaffold-hbar", "--name", "Phone Wallet", "--yes", "--no-git"], quiet)).toBe(0); + expect(readdirSync(join(target, "packages")).sort()).toEqual(["desktop", "mobile", "nextjs"]); + const scripts = json(join(target, "package.json")).scripts; + expect(Object.keys(scripts).filter((k) => k.includes("extension"))).toEqual([]); + expect(scripts.build).toBe("pnpm desktop:build && pnpm next:build"); + expect(scripts["next:dev"]).toBe("pnpm --filter @sh/nextjs dev"); + expect(read(join(target, "README.md"))).not.toMatch(/packages\/extension|chrome-mv3/); + expect(read(join(target, ".github/workflows/ci.yaml"))).not.toMatch(/extension:build/); + expect(read(join(target, ".github/workflows/ci.yaml"))).toMatch(/mobile:export/); + expect(existsSync(join(target, "packages/extension/public"))).toBe(false); + // The dapp reads the identity from the root, so it works without the extension package. + expect(read(join(target, "packages/nextjs/utils/wallet.ts"))).toContain('from "../../../wallet.identity.json"'); + expect(json(join(target, "packages/nextjs/package.json")).dependencies["@sh/extension"]).toBeUndefined(); + await expect(main([join(tmp(), "x"), "--platforms", "tv", "--yes"], quiet)).rejects.toThrow(/--platforms: tv isn't a platform/); + }); + + it("with --scaffold-hbar and every platform, produces the same project as create-scaffold-hbar's copy step followed by pnpm wallet:identity", async () => { const base = tmp(); const viaCli = join(base, "same"); - expect(await main([viaCli, ...FLAGS], quiet)).toBe(0); + expect(await main([viaCli, "--scaffold-hbar", ...FLAGS], quiet)).toBe(0); // create-scaffold-hbar: copy the tree (CREATE_SCAFFOLD_HBAR_TEMPLATE_DIR path), process template.json, then the // project's own `pnpm wallet:identity` with the same answers. @@ -117,14 +168,14 @@ describe("create-clip-wallet ", () => { expect(await main(["identity", "--root", viaHbar, ...FLAGS], quiet)).toBe(0); expect(tree(viaHbar)).toEqual(tree(viaCli)); - const keyA = json(join(viaCli, "packages/extension/wallet.identity.json")).extension.key; - const keyB = json(join(viaHbar, "packages/extension/wallet.identity.json")).extension.key; + const keyA = json(join(viaCli, "wallet.identity.json")).extension.key; + const keyB = json(join(viaHbar, "wallet.identity.json")).extension.key; const norm = (dir: string, key: string, f: string) => read(join(dir, f)).replaceAll(key, "KEY").replaceAll(extensionIdFromKey(key), "EXTID"); for (const f of tree(viaCli)) { - if (f.endsWith(".png")) expect(readFileSync(join(viaHbar, f)).equals(readFileSync(join(viaCli, f))), f).toBe(true); + if (/\.(?:png|icns|ico)$/.test(f)) expect(readFileSync(join(viaHbar, f)).equals(readFileSync(join(viaCli, f))), f).toBe(true); else expect(norm(viaHbar, keyB, f), f).toBe(norm(viaCli, keyA, f)); } - }); + }, 60_000); it("refuses Clip Wallet's identity, bad answers and full folders, and writes nothing", async () => { const base = tmp(); @@ -132,17 +183,19 @@ describe("create-clip-wallet ", () => { /rdns: org.coldai.clipwallet belongs to Clip Wallet[\s\S]*name: Clip Wallet is taken/, ); await expect(main([join(base, "b"), "--accent", "green", "--yes"], quiet)).rejects.toThrow(/theme.accent: use a hex colour/); - expect(existsSync(join(base, "a"))).toBe(false); - expect(existsSync(join(base, "b"))).toBe(false); + await expect(main([join(base, "c"), "--id", "org.coldai.clipwallet", "--yes"], quiet)).rejects.toThrow(/appId: org.coldai.clipwallet belongs to Clip Wallet/); + await expect(main([join(base, "d"), "--languages", "en,xx", "--yes"], quiet)).rejects.toThrow(/languages\.1: use the languages the wallet ships/); + await expect(main([join(base, "e"), "--logo", join(base, "nope.png"), "--yes"], quiet)).rejects.toThrow(/logo: .*nope\.png isn't there/); + for (const d of ["a", "b", "c", "d", "e"]) expect(existsSync(join(base, d))).toBe(false); writeFileSync(join(base, "keep.txt"), "x"); await expect(main([base, "--yes"], quiet)).rejects.toThrow(/isn't empty/); }); it("prompts for what the flags leave out, re-asking after a bad answer", async () => { const target = join(tmp(), "my-wallet"); - const replies = ["", "com.me.wallet", "not-a-colour", "#123456", "evm:*, solana"]; + const replies = ["extension, tv", "mobile, extension", "", "", "com.me.wallet", "", "not-a-colour", "#123456", "", "evm:*, solana", "en, xx", "de, en"]; const asked: string[] = []; - await main([target], { + await main([target, "--no-git"], { log: () => {}, question: async (q) => { asked.push(q); @@ -150,14 +203,24 @@ describe("create-clip-wallet ", () => { }, }); expect(asked).toEqual([ + "Platforms (extension, desktop, mobile) (extension, desktop, mobile): ", + "Platforms (extension, desktop, mobile) (extension, desktop, mobile): ", + "Add the Scaffold-HBAR demo dapp? (y/N) (n): ", "Wallet name (My Wallet): ", "Reverse domain you own (EIP-6963 rdns) (com.example.mywallet): ", + "App id for the desktop and phone apps (com.me.wallet): ", "Accent colour (#4F46E5): ", "Accent colour (#4F46E5): ", + "Logo (.png or .svg; empty = a starter mark): ", "Networks (evm:*, hedera, solana, bitcoin): ", + "Languages (en, de, fr, es, pt-BR, it, tr, ja, ko, zh-Hans, ar, hi): ", + "Languages (en, de, fr, es, pt-BR, it, tr, ja, ko, zh-Hans, ar, hi): ", ]); - expect(json(join(target, "packages/extension/wallet.identity.json"))).toMatchObject({ name: "My Wallet", rdns: "com.me.wallet" }); - expect(read(join(target, "packages/extension/clip.config.ts"))).toContain('networks: ["evm:*", "solana"]'); + expect(readdirSync(join(target, "packages")).sort()).toEqual(["extension", "mobile"]); + expect(json(join(target, "wallet.identity.json"))).toMatchObject({ name: "My Wallet", rdns: "com.me.wallet" }); + expect(json(join(target, "wallet.identity.json")).appId).toBeUndefined(); + expect(read(join(target, "clip.config.ts"))).toContain('networks: ["evm:*", "solana"]'); + expect(read(join(target, "clip.config.ts"))).toContain('languages: ["de", "en"]'); }); it("never switches mainnet on", () => { @@ -165,20 +228,83 @@ describe("create-clip-wallet ", () => { }); }); +/** A 1024×1024 opaque PNG logo: a dark blue field with a white square in the middle. */ +function logoPng(): Buffer { + const lines: Buffer[] = []; + for (let y = 0; y < 1024; y++) { + const row = Buffer.alloc(1 + 1024 * 3); + for (let x = 0; x < 1024; x++) { + const inside = x >= 256 && x < 768 && y >= 256 && y < 768; + row.set(inside ? [255, 255, 255] : [0x12, 0x34, 0x56], 1 + x * 3); + } + lines.push(row); + } + const crcTable = Array.from({ length: 256 }, (_, n) => { + let c = n; + for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + return c >>> 0; + }); + const crc = (b: Buffer) => { + let c = 0xffffffff; + for (const x of b) c = crcTable[(c ^ x) & 0xff]! ^ (c >>> 8); + return (c ^ 0xffffffff) >>> 0; + }; + const chunk = (t: string, d: Buffer) => { + const len = Buffer.alloc(4); + len.writeUInt32BE(d.length); + const body = Buffer.concat([Buffer.from(t), d]); + const c = Buffer.alloc(4); + c.writeUInt32BE(crc(body)); + return Buffer.concat([len, body, c]); + }; + const ihdr = Buffer.alloc(13); + ihdr.writeUInt32BE(1024, 0); + ihdr.writeUInt32BE(1024, 4); + ihdr[8] = 8; + ihdr[9] = 2; + return Buffer.concat([Buffer.from("89504e470d0a1a0a", "hex"), chunk("IHDR", ihdr), chunk("IDAT", deflateSync(Buffer.concat(lines))), chunk("IEND", Buffer.alloc(0))]); +} + describe("in a project", () => { it("identity keeps the extension key (and id) unless --new-key", async () => { const target = join(tmp(), "w"); await main([target, ...FLAGS, "--no-git"], quiet); - const key = json(join(target, "packages/extension/wallet.identity.json")).extension.key; + const key = json(join(target, "wallet.identity.json")).extension.key; await main(["identity", "--root", target, "--name", "Acme Pro", "--rdns", "com.acme.pro", "--yes"], quiet); - const id = json(join(target, "packages/extension/wallet.identity.json")); + const id = json(join(target, "wallet.identity.json")); expect(id).toMatchObject({ name: "Acme Pro", rdns: "com.acme.pro", extension: { key } }); // The accent stays what clip.config.ts says. - expect(read(join(target, "packages/extension/clip.config.ts"))).toContain('accent: "#0B7A3B"'); + expect(read(join(target, "clip.config.ts"))).toContain('accent: "#0B7A3B"'); await main(["identity", "--root", target, "--name", "Acme Pro", "--rdns", "com.acme.pro", "--new-key", "--yes"], quiet); - expect(json(join(target, "packages/extension/wallet.identity.json")).extension.key).not.toBe(key); + expect(json(join(target, "wallet.identity.json")).extension.key).not.toBe(key); }); + it("brand renders every platform's icons from a PNG logo, and a later accent change keeps the logo", async () => { + const target = join(tmp(), "w"); + await main([target, ...FLAGS, "--no-git"], quiet); + const logo = join(tmp(), "logo.png"); + writeFileSync(logo, logoPng()); + const out: string[] = []; + expect(await main(["brand", "--root", target, "--logo", logo], { ...quiet, log: (s) => out.push(s) })).toBe(0); + expect(out.join("\n")).toMatch(/Rendered 24 icon files for extension, desktop, mobile/); + expect(existsSync(join(target, "icon.png")) && !existsSync(join(target, "icon.svg"))).toBe(true); + expect(json(join(target, "wallet.identity.json")).icon).toBe("./icon.png"); + // An opaque logo fills the iOS icon; the extension's 16 px icon is the logo scaled down (blue edge, white middle). + const ios = decodePng(readFileSync(join(target, "packages/mobile/assets/icon.png"))); + expect([...ios.data.subarray(0, 4)]).toEqual([0x12, 0x34, 0x56, 255]); + const ext16 = decodePng(readFileSync(join(target, "packages/extension/public/icon/16.png"))); + expect([...ext16.data.subarray(0, 3)]).toEqual([0x12, 0x34, 0x56]); + expect([...ext16.data.subarray((8 * 16 + 8) * 4, (8 * 16 + 8) * 4 + 3)]).toEqual([255, 255, 255]); + // Android's themed icon and the macOS template: the logo's shape (the white square), not its background. + const mono = decodePng(readFileSync(join(target, "packages/mobile/assets/adaptive-monochrome.png"))); + expect(mono.data[(512 * 1024 + 512) * 4 + 3]).toBe(255); + expect(mono.data[(212 * 1024 + 512) * 4 + 3]).toBe(0); + // A new accent later re-renders with the same logo (never back to the starter mark). + await main(["identity", "--root", target, "--name", "Acme Wallet", "--rdns", "com.acme.wallet", "--accent", "#7A0B3B", "--yes"], quiet); + expect(json(join(target, "wallet.identity.json")).icon).toBe("./icon.png"); + expect([...decodePng(readFileSync(join(target, "packages/mobile/assets/icon.png"))).data.subarray(0, 3)]).toEqual([0x12, 0x34, 0x56]); + }, 60_000); + it("listings regenerates the drafts from wallet.identity.json", async () => { const target = join(tmp(), "w"); await main([target, ...FLAGS, "--no-git"], quiet); @@ -208,6 +334,7 @@ describe("in a project", () => { it("refuses to run outside a project", async () => { await expect(main(["identity", "--root", tmp(), "--yes"], quiet)).rejects.toThrow(/isn't a Clip Wallet project/); + await expect(main(["brand", "--root", tmp()], quiet)).rejects.toThrow(/isn't a Clip Wallet project/); }); }); @@ -215,6 +342,14 @@ describe("pieces", () => { it("parses commands and options", () => { expect(parseArgs(["identity", "--name", "X", "-y"])).toMatchObject({ command: "identity", name: "X", yes: true }); expect(parseArgs(["./identity"])).toMatchObject({ dir: "./identity" }); + expect(parseArgs(["w", "--platforms", "mobile, extension", "--scaffold-hbar", "--id", "com.x.y", "--logo", "l.png", "--languages", "en,de"])).toMatchObject({ + platforms: ["extension", "mobile"], + scaffoldHbar: true, + id: "com.x.y", + logo: "l.png", + languages: ["en", "de"], + }); + expect(parseArgs(["w", "--icon", "l.svg"]).logo).toBe("l.svg"); expect(() => parseArgs(["--colour", "x"])).toThrow(/Unknown option --colour/); }); diff --git a/packages/desktop-kit/src/main/index.ts b/packages/desktop-kit/src/main/index.ts index 54b0169..7063c80 100644 --- a/packages/desktop-kit/src/main/index.ts +++ b/packages/desktop-kit/src/main/index.ts @@ -80,6 +80,8 @@ const DEV_ORIGIN = DEV_URL ? new URL(DEV_URL).origin : undefined; if (!app.isPackaged && process.env.CLIP_DESKTOP_USER_DATA) { app.setPath("userData", process.env.CLIP_DESKTOP_USER_DATA === "temp" ? mkdtempSync(join(tmpdir(), "clip-desktop-")) : process.env.CLIP_DESKTOP_USER_DATA); } +// The wallet's name for menus, dialogs and the data folder (packaged builds already carry it as productName). +if (app.getName() !== DESKTOP.config.name) app.setName(DESKTOP.config.name); app.setAppUserModelId(DESKTOP.appId); registerAppScheme(); // Never offer Chromium's own password manager / autofill for anything here. diff --git a/packages/extension-kit/src/wxt.ts b/packages/extension-kit/src/wxt.ts index baaf707..0362f43 100644 --- a/packages/extension-kit/src/wxt.ts +++ b/packages/extension-kit/src/wxt.ts @@ -18,7 +18,17 @@ import { createHash } from "node:crypto"; import { existsSync, readFileSync } from "node:fs"; import { extname, join, resolve } from "node:path"; -import { ConfigError, WALLETCONNECT_ENV, defineConfig as defineClipConfig, isMainnetEnabled, mainnetProblems, walletKey, type ClipConfig, type ClipConfigInput } from "@clip-wallet/config"; +import { + ConfigError, + WALLETCONNECT_ENV, + defineConfig as defineClipConfig, + isMainnetEnabled, + mainnetProblems, + slugOfName, + walletKey, + type ClipConfig, + type ClipConfigInput, +} from "@clip-wallet/config"; import { createTonModule } from "@clip-wallet/chains-ton"; import { walletNetworks } from "@clip-wallet/engine/catalog"; import { SANDBOX_CSP, SANDBOX_PAGE } from "@clip-wallet/plugins"; @@ -38,8 +48,13 @@ export const CONFIG_MODULE = "virtual:clip-wallet/config"; export interface ClipWalletOptions { /** The default export of clip.config.ts (already validated by defineConfig). */ config: ClipConfig; - /** The extension project directory (clip.config.ts, icon, .env, package.json). Default: process.cwd(). */ + /** The extension project directory (package.json, src/, .env). Default: process.cwd(). */ root?: string; + /** + * Where clip.config.ts, its icon, MAINNET.md and the wallet-wide .env are, relative to root or absolute. Default: root. + * A create-clip-wallet project keeps one clip.config.ts at its root for every platform: configDir "../..". + */ + configDir?: string; /** Version for the manifest and TON Connect DeviceInfo. Default: the project's package.json version. */ version?: string; /** Build environment. Default: process.env plus CLIP_* values from /.env (process.env wins). */ @@ -122,13 +137,14 @@ export function openChecklistItems(root: string): string[] { export function clipWallet(options: ClipWalletOptions): UserConfig { const root = options.root ?? process.cwd(); - const env = { ...readClipEnv(join(root, ".env")), ...process.env, ...options.env }; - const config = resolveConfig(options.config, env, root); + const configDir = resolve(root, options.configDir ?? "."); + const env = { ...readClipEnv(join(configDir, ".env")), ...readClipEnv(join(root, ".env")), ...process.env, ...options.env }; + const config = resolveConfig(options.config, env, configDir); const mocks = options.mocks ?? env.CLIP_MOCKS === "1"; const source = options.sourceConditions ?? devConditions(); const version = options.version ?? (JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { version?: string }).version ?? "0.0.0"; const key = walletKey(config); - const icon = iconDataUri(root, config.icon); + const icon = iconDataUri(configDir, config.icon); const testnet = !isMainnetEnabled(config); const security = securityFor(config, env); @@ -163,6 +179,8 @@ export function clipWallet(options: ClipWalletOptions): UserConfig { return { srcDir: "src", outDir: mocks ? ".output-fixtures" : ".output", + // Store uploads named after the wallet: acme-wallet-1.2.0-chrome.zip, acme-wallet-1.2.0-sources.zip. + zip: { artifactTemplate: `${slugOfName(config.name)}-{{version}}-{{browser}}.zip`, sourcesTemplate: `${slugOfName(config.name)}-{{version}}-sources.zip` }, // Explicit imports only (unimport would otherwise inject e.g. `storage` into the vault). // @ts-expect-error autoImport is an unimport option that WXT passes through but doesn't type. imports: { autoImport: false, eslintrc: { enabled: false } }, diff --git a/packages/extension-kit/test/wxt.test.ts b/packages/extension-kit/test/wxt.test.ts index d0c2524..4cda923 100644 --- a/packages/extension-kit/test/wxt.test.ts +++ b/packages/extension-kit/test/wxt.test.ts @@ -1,5 +1,5 @@ import { generateKeyPairSync } from "node:crypto"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; @@ -97,6 +97,24 @@ describe("clipWallet()", () => { expect(String(ok.manifest({ browser: "chrome", manifestVersion: 3 }).description)).not.toMatch(/Test networks/); }); + it("reads the icon, MAINNET.md and the wallet-wide .env from configDir (one clip.config.ts for every platform)", () => { + const wallet_ = project(); + const ext = join(wallet_, "packages", "extension"); + mkdirSync(ext, { recursive: true }); + writeFileSync(join(ext, "package.json"), JSON.stringify({ name: "@sh/extension", version: "0.2.0" })); + rmSync(join(wallet_, "package.json")); + writeFileSync(join(wallet_, ".env"), `CLIP_WALLETCONNECT_PROJECT_ID=${"c".repeat(32)}\n`); + const cfg = wallet({ config: acme({ icon: "./icon.svg" }), root: ext, configDir: "../..", env: undefined }); + const v = vite(cfg); + expect(JSON.parse(v.define.__CLIP_IDENTITY__!).icon).toMatch(/^data:image\/svg\+xml;base64,/); + expect(JSON.parse(v.define.__CLIP_WALLETCONNECT__!)).toBe(true); + expect(JSON.parse(v.define.__CLIP_TON_CONNECT__!).appVersion).toBe("0.2.0"); + expect((cfg as unknown as { zip: { artifactTemplate: string } }).zip.artifactTemplate).toBe("acme-wallet-{{version}}-{{browser}}.zip"); + writeFileSync(join(wallet_, "MAINNET.md"), "- [ ] audited\n"); + const config = acme({ mainnet: { enabled: true, acknowledged: MAINNET_ACKNOWLEDGEMENT }, homepage: "https://acme.example", extension: { key: publicKey } }); + expect(() => wallet({ config, root: ext, configDir: wallet_, env: { CLIP_WALLETCONNECT_PROJECT_ID: "b".repeat(32) } })).toThrow(/MAINNET\.md: audited/); + }); + it("refuses an icon from someone else's server", () => { expect(() => wallet({ config: acme({ icon: "https://cdn.example/icon.png" }), root: project() })).toThrow(/ship the icon with the extension/); }); diff --git a/templates/scaffold-hbar-clip-wallet/packages/extension/.env.example b/templates/scaffold-hbar-clip-wallet/.env.example similarity index 100% rename from templates/scaffold-hbar-clip-wallet/packages/extension/.env.example rename to templates/scaffold-hbar-clip-wallet/.env.example diff --git a/templates/scaffold-hbar-clip-wallet/.github/scripts/check-scaffold.sh b/templates/scaffold-hbar-clip-wallet/.github/scripts/check-scaffold.sh index a849dd5..97b72b6 100755 --- a/templates/scaffold-hbar-clip-wallet/.github/scripts/check-scaffold.sh +++ b/templates/scaffold-hbar-clip-wallet/.github/scripts/check-scaffold.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash -# Checks a freshly scaffolded project: files, no secrets, install, identity, the harness, types, both builds, the -# dapp serving / and /debug, the extension manifest carrying the new identity, and the mainnet gate. With a second +# Checks a freshly scaffolded project: files, no secrets, install, identity, the harness, types (every platform), the +# builds (extension, desktop, dapp), the dapp serving / and /debug, the extension manifest carrying the new identity, +# and the mainnet gate. With a second # argument (a project made by create-clip-wallet with the same answers) it also checks both trees are the same. set -euo pipefail APP="$(cd "$1" && pwd)" @@ -12,7 +13,8 @@ fail() { echo "FAIL: $*" >&2; exit 1; } step "Required files" for f in README.md AGENTS.md CLAUDE.md llms.txt LICENCE package.json pnpm-workspace.yaml .harness/spec.yaml \ - packages/extension/clip.config.ts packages/extension/wallet.identity.json packages/extension/MAINNET.md \ + clip.config.ts wallet.identity.json MAINNET.md docs/signing.md packages/extension/wxt.config.ts \ + packages/desktop/electron.vite.config.ts packages/mobile/app.config.ts \ packages/nextjs/app/debug/page.tsx tools/harness/check.mjs; do [ -f "$f" ] || fail "missing $f" done @@ -24,9 +26,9 @@ if git ls-files | grep -E '(^|/)\.env(\.[^/]*)?$|\.pem$|(^|/)\.keys/' | grep -v step "Install and identity" pnpm install pnpm wallet:identity --name "Fresh Scaffold" --rdns com.example.freshscaffold --yes -node -e 'const i=require("./packages/extension/wallet.identity.json"); if(i.name!=="Fresh Scaffold"||!i.extension?.key) process.exit(1)' || fail "identity not written" -[ -f packages/extension/.keys/extension.pem ] || fail "no extension key" -git check-ignore -q packages/extension/.keys/extension.pem || fail "the extension key isn't gitignored" +node -e 'const i=require("./wallet.identity.json"); if(i.name!=="Fresh Scaffold"||!i.extension?.key) process.exit(1)' || fail "identity not written" +[ -f .keys/extension.pem ] || fail "no extension key" +git check-ignore -q .keys/extension.pem || fail "the extension key isn't gitignored" step "Harness, types, builds" pnpm harness @@ -36,13 +38,13 @@ pnpm build step "The extension carries the new identity" node -e ' const m = require("./packages/extension/.output/chrome-mv3/manifest.json"); -const id = require("./packages/extension/wallet.identity.json"); +const id = require("./wallet.identity.json"); if (m.name !== id.name) throw new Error("manifest name " + m.name); if (m.key !== id.extension.key) throw new Error("manifest key differs from wallet.identity.json"); console.log("manifest ok:", m.name);' step "Mainnet is gated" -cfg=packages/extension/clip.config.ts +cfg=clip.config.ts cp "$cfg" "$cfg.orig" # sed -i.bak works with both GNU and BSD sed. sed -i.bak -e 's/^ mainnet: false,$/ mainnet: { enabled: true, acknowledged: MAINNET_ACKNOWLEDGEMENT },/' \ @@ -66,7 +68,7 @@ done if [ -n "$CLI" ]; then step "create-clip-wallet made the same project" # Excluded: what install/build/serve create, and the two files that carry each wallet's own random extension key. - diff -rq -x .git -x node_modules -x .keys -x .output -x .wxt -x .next -x serve.log -x '*.tsbuildinfo' -x next-env.d.ts \ + diff -rq -x .git -x node_modules -x .keys -x .output -x .wxt -x .next -x out -x .expo -x serve.log -x '*.tsbuildinfo' -x next-env.d.ts \ -x pnpm-lock.yaml -x wallet.identity.json -x listings "$CLI" "$APP" || fail "the two projects differ (beyond the per-wallet key)" fi echo "Fresh scaffold OK" diff --git a/templates/scaffold-hbar-clip-wallet/.github/workflows/ci.yaml b/templates/scaffold-hbar-clip-wallet/.github/workflows/ci.yaml index 4ec883c..e3d9560 100644 --- a/templates/scaffold-hbar-clip-wallet/.github/workflows/ci.yaml +++ b/templates/scaffold-hbar-clip-wallet/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ permissions: jobs: wallet: - name: Harness, types, extension and dapp builds + name: Harness, types and every platform's build runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 22.18 - name: Install run: pnpm install @@ -37,15 +37,34 @@ jobs: - name: Types run: pnpm check-types +# platform:nextjs - name: Lint run: pnpm lint +# /platform:nextjs +# platform:extension - name: Extension build run: pnpm extension:build +# /platform:extension +# platform:desktop + - name: Desktop build (unsigned Linux tar.gz; signed installers need docs/signing.md) + run: | + pnpm desktop:build + pnpm --filter @sh/desktop exec electron-builder --linux tar.gz --config electron-builder.config.cjs --publish never + +# /platform:desktop +# platform:mobile + - name: Phone app JavaScript bundles (iOS and Android) + run: pnpm mobile:export + +# /platform:mobile +# platform:nextjs - name: Dapp build run: pnpm next:build +# /platform:nextjs + - name: Mainnet stays off until its checklist is done run: | set +e diff --git a/templates/scaffold-hbar-clip-wallet/.github/workflows/fresh-scaffold.yaml b/templates/scaffold-hbar-clip-wallet/.github/workflows/fresh-scaffold.yaml index fa1b31d..369f51f 100644 --- a/templates/scaffold-hbar-clip-wallet/.github/workflows/fresh-scaffold.yaml +++ b/templates/scaffold-hbar-clip-wallet/.github/workflows/fresh-scaffold.yaml @@ -45,7 +45,7 @@ jobs: - name: The same wallet with create-clip-wallet working-directory: ${{ runner.temp }} - run: npx --yes create-clip-wallet@latest cli/app --name "Fresh Scaffold" --rdns com.example.freshscaffold --yes + run: npx --yes create-clip-wallet@latest cli/app --scaffold-hbar --name "Fresh Scaffold" --rdns com.example.freshscaffold --yes - name: Check the project run: .github/scripts/check-scaffold.sh "${{ runner.temp }}/app" "${{ runner.temp }}/cli/app" diff --git a/templates/scaffold-hbar-clip-wallet/.harness/acceptance-contract.json b/templates/scaffold-hbar-clip-wallet/.harness/acceptance-contract.json index c2172b9..f309e53 100644 --- a/templates/scaffold-hbar-clip-wallet/.harness/acceptance-contract.json +++ b/templates/scaffold-hbar-clip-wallet/.harness/acceptance-contract.json @@ -8,7 +8,7 @@ "statement": "The home page names this wallet (not Clip Wallet) and explains how to load the extension when it isn't installed.", "howToVerify": [ "Open http://localhost:3000/ in a browser without the extension.", - "Confirm the heading is the name in packages/extension/wallet.identity.json.", + "Confirm the heading is the name in wallet.identity.json.", "Confirm step 1 says the wallet isn't installed and lists pnpm extension:build and Load unpacked." ], "severity": "critical", diff --git a/templates/scaffold-hbar-clip-wallet/.harness/spec.md b/templates/scaffold-hbar-clip-wallet/.harness/spec.md index 2ac55fe..0c3c9ae 100644 --- a/templates/scaffold-hbar-clip-wallet/.harness/spec.md +++ b/templates/scaffold-hbar-clip-wallet/.harness/spec.md @@ -1,8 +1,8 @@ # Product spec -This project is **a non-custodial browser wallet with its own identity**, built on the Clip Wallet kit, and a -Scaffold-HBAR dapp that shows it working on Hedera testnet. Pre-release: **test networks only** until the owner -completes `packages/extension/MAINNET.md`. +This project is **a non-custodial wallet with its own identity** on every platform it ships (browser extension, desktop +app, phone app; one `clip.config.ts`), built on the Clip Wallet kit, and a Scaffold-HBAR dapp that shows it working on +Hedera testnet. Pre-release: **test networks only** until the owner completes `MAINNET.md`. ## Principles (from the kit) diff --git a/templates/scaffold-hbar-clip-wallet/.harness/validators/static.json b/templates/scaffold-hbar-clip-wallet/.harness/validators/static.json index 9f4f3bf..0ba2abc 100644 --- a/templates/scaffold-hbar-clip-wallet/.harness/validators/static.json +++ b/templates/scaffold-hbar-clip-wallet/.harness/validators/static.json @@ -10,20 +10,20 @@ "AGENTS.md", "llms.txt", ".harness/spec.yaml", - "packages/extension/wallet.identity.json", - "packages/extension/clip.config.ts", + "wallet.identity.json", + "clip.config.ts", "packages/extension/wxt.config.ts", - "packages/extension/MAINNET.md", + "MAINNET.md", "packages/nextjs/app/page.tsx", "packages/nextjs/app/debug/page.tsx", "tools/harness/check.mjs" ], - "forbidden": [".env", "packages/extension/.env.production", "packages/extension/.keys/extension.pem.bak"] + "forbidden": [".env", "packages/extension/.env.production", ".keys/extension.pem.bak"] }, "textAssertions": [ { "file": "packages/nextjs/app/page.tsx", "contains": ["useSignTypedData", "verifyTypedData", "Sign typed data"] }, { "file": "packages/extension/wxt.config.ts", "contains": ["clipWallet({"] }, - { "file": "packages/extension/clip.config.ts", "contains": ["mainnet: false,"] }, + { "file": "clip.config.ts", "contains": ["mainnet: false,"] }, { "file": "packages/nextjs/scaffold.config.ts", "contains": ["[chains.hederaTestnet]"] } ] } diff --git a/templates/scaffold-hbar-clip-wallet/AGENTS.md b/templates/scaffold-hbar-clip-wallet/AGENTS.md index d64598f..c3e53dc 100644 --- a/templates/scaffold-hbar-clip-wallet/AGENTS.md +++ b/templates/scaffold-hbar-clip-wallet/AGENTS.md @@ -1,22 +1,38 @@ # AGENTS.md: a wallet on the Clip Wallet kit -This project is a non-custodial browser wallet (`packages/extension`) built on `@clip-wallet/extension-kit`, plus a -Scaffold-HBAR dapp (`packages/nextjs`) that connects to it on Hedera testnet. The wallet's code lives in the kit -packages; this project holds the identity, the configuration and the dapp. Map: `llms.txt`. Product: `.harness/spec.md`, -`.harness/prd.md`. +This project is a non-custodial wallet on the Clip Wallet kit, for every platform it ships, all described by one +`clip.config.ts` at the root: + +- `packages/extension`: the browser extension, on `@clip-wallet/extension-kit` (WXT); + + +- `packages/desktop`: the desktop app for macOS, Windows and Linux, on `@clip-wallet/desktop-kit` (Electron); + + +- `packages/mobile`: the phone app for iOS and Android, on `@clip-wallet/mobile-kit` (Expo); + + +- `packages/nextjs`: a Scaffold-HBAR dapp that connects to the wallet on Hedera testnet. + + +The wallet's code lives in the kit packages; this project holds the identity, the configuration, the icons and +one-line entrypoints. Map: `llms.txt`. Product: `.harness/spec.md`, `.harness/prd.md`. ## Rules that never break 1. **Never handle key material here.** Recovery phrases, private keys, seeds and signing live only in `@clip-wallet/vault`, inside the kit's background. Don't import `@clip-wallet/vault`, `@scure/bip39`/`bip32`, noble signing APIs, `viem/accounts`, `ethers` wallets, Hedera `PrivateKey` or Solana `Keypair` in this project. 2. **The identity is this wallet's own.** Never set `rdns` to `org.coldai.*` or the name to "Clip Wallet". Identity lives - in `packages/extension/wallet.identity.json`; change it with `pnpm wallet:identity`, not by hand-editing the key. -3. **The security floor stays.** `packages/extension/wxt.config.ts` builds through `clipWallet()`; never switch the - phishing lists off, filter them, or define `__CLIP_SECURITY__`. -4. **Testnet by default.** Never turn mainnet on and never tick a box in `packages/extension/MAINNET.md`: that is the - owner's decision, and the harness and the build both refuse it until the checklist is done. -5. **Secrets stay out of git and out of logs.** `.env`, `.env.local`, `packages/extension/.keys/` and `*.pem` are - gitignored; keep them so. Never print them. API keys go in `.env` as `CLIP_*` (wallet) or `NEXT_PUBLIC_*` (dapp; public by definition). + in `wallet.identity.json`; change it with `pnpm wallet:identity`, not by hand-editing the key. +3. **The security floor stays.** Every platform builds through its kit: `clipWallet()` (`packages/extension/wxt.config.ts`), + `clipDesktop()` (`packages/desktop/electron.vite.config.ts`), `expoConfig()` and `withClipWallet()` + (`packages/mobile/app.config.ts`, `metro.config.js`). Never switch the phishing lists off, filter them, or define + `__CLIP_SECURITY__`. +4. **Testnet by default.** Never turn mainnet on and never tick a box in `MAINNET.md`: that is the owner's decision, + and the harness and every build refuse it until the checklist is done. +5. **Secrets stay out of git and out of logs.** `.env`, `.env.local`, `.keys/` and `*.pem` are gitignored; keep them so. + Never print them. API keys go in `.env` as `CLIP_*` (wallet) or `NEXT_PUBLIC_*` (dapp; public by definition). + Signing certificates and store credentials only ever live in CI secrets (`docs/signing.md`). 6. **Kit packages are pinned.** `@clip-wallet/*` and `create-clip-wallet` use one exact version (`"0.1.0"`, never `^`/`~`). 7. **Every dapp request is decoded before approval.** Don't add code that signs or sends outside the approval path. Undecodable requests are blind signing, off by default. @@ -25,17 +41,26 @@ packages; this project holds the identity, the configuration and the dapp. Map: ```bash pnpm install && pnpm harness && pnpm check-types && pnpm build ``` -`pnpm harness` is fast (no network): run it after every change. `pnpm build` builds the extension and the dapp. +`pnpm harness` is fast (no network): run it after every change. `pnpm build` builds the extension, the desktop app and +the dapp (whichever this project has); `pnpm mobile:export` builds the phone app's JavaScript bundles. ## Layout ``` -packages/extension/wallet.identity.json name, description, rdns, homepage, icon, extension public key -packages/extension/clip.config.ts theme, networks, routing, hardware, passkeys, services, mainnet (typed) -packages/extension/wxt.config.ts clipWallet({ config }): the whole build -packages/extension/src/entrypoints/ one-line entrypoints into the kit -packages/extension/MAINNET.md the owner's mainnet checklist -packages/nextjs/ Scaffold-HBAR dapp: app/page.tsx (demo), app/clip-connect (Clip Connect demo), app/debug, contracts/ +wallet.identity.json name, description, rdns, homepage, icon, app id, extension public key +clip.config.ts theme, networks, languages, scheme, routing, hardware, passkeys, services, mainnet (typed) +icon.svg (or icon.png) the logo; every platform's icons are rendered from it (pnpm wallet:brand) +MAINNET.md the owner's mainnet checklist +packages/extension/wxt.config.ts clipWallet({ config }): the whole extension build +packages/extension/src/entrypoints/ one-line entrypoints into the kit +packages/desktop/electron.vite.config.ts clipDesktop({ config }): the whole desktop build +packages/desktop/electron-builder.config.cjs electronBuilderConfig(): installers for macOS, Windows, Linux +packages/desktop/src/ one-line entrypoints into the kit; build/ and src/renderer/public/tray/ are icons +packages/mobile/app.config.ts expoConfig(): name, ids, icons, splash, permissions +packages/mobile/metro.config.js withClipWallet(): the resolved config for the app +packages/mobile/index.ts registerClipWallet(); assets/ are icons +packages/nextjs/ Scaffold-HBAR dapp: app/page.tsx (demo), app/clip-connect (Clip Connect demo), app/debug, contracts/ docs/listings/ listing drafts (generated) +docs/signing.md code signing and store accounts: what to set (never the values) tools/harness/check.mjs the rules above, as checks ``` @@ -43,17 +68,30 @@ tools/harness/check.mjs the rules above, as checks Each ends with `pnpm harness && pnpm check-types && pnpm build`. ### Change the wallet's identity -1. `pnpm wallet:identity --name "…" --rdns [--homepage https://…] [--description "…"] [--icon path.svg|png] [--walletconnect-project-id ]`. -2. It rewrites `wallet.identity.json`, the page titles, the icon (only when asked or first run) and `docs/listings/`, and - keeps the extension key. `--new-key` changes the extension id: only when the owner asks. -3. Never commit `packages/extension/.keys/extension.pem` or the `.env` files it writes. +1. `pnpm wallet:identity --name "…" --rdns [--id ] [--homepage https://…] [--description "…"] [--logo path.png|svg] [--walletconnect-project-id ]`. +2. It rewrites `wallet.identity.json`, the page titles, the icons (only when asked or first run) and `docs/listings/`, and + keeps the extension key. `--new-key` changes the extension id: only when the owner asks. Changing `--id` changes the + desktop and phone app ids: a store treats that as a different app. +3. Never commit `.keys/extension.pem` or the `.env` files it writes. ### Change the look 1. `theme` in `clip.config.ts`: `accent`, `accentText` (contrast with the accent at least 3:1, or the build fails), `font`, `radius` (0–32). -2. The icon: replace `packages/extension/icon.svg` and `public/icon/{16,32,48,128}.png` (or `pnpm wallet:identity --icon`). +2. The icon: `pnpm wallet:brand --logo path.png` renders every platform's icons (extension sizes, `.icns`, `.ico`, Linux + PNGs, tray, iOS, Android adaptive and monochrome, splash) from one logo. Don't hand-edit the generated PNGs. 3. Screens are the kit's; they read the theme and the name. Don't fork them into this project. +### Languages and deep links +1. `languages` in `clip.config.ts`: the subset of `en`, `de`, `fr`, `es`, `pt-BR`, `it`, `tr`, `ja`, `ko`, `zh-Hans`, `ar`, + `hi` that Settings → Language offers; the first is the fallback. +2. `scheme` (default: the name in lower case, no spaces): the desktop and phone apps' deep links (`://wc?uri=…`). + +### Add or remove a platform +The platforms are folders: `packages/extension`, `packages/desktop`, `packages/mobile`. To add one later, create a +project with `npx create-clip-wallet tmp --platforms --name "" --yes`, copy its `packages/` +folder and its root `package.json` scripts over, run `pnpm install && pnpm wallet:brand`. To remove one, delete its +folder and its scripts. + ### Turn networks on or off 1. `networks` in `clip.config.ts`: `"evm:*"` or `"evm:"`, `"hedera"`, `"solana"`, `"bitcoin"`, `"sui"`, `"aptos"`, `"cardano"`, `"substrate"`, `"starknet"`, `"ton"`, `"near"`, `"stellar"`, `"tezos"`, `"algorand"`. @@ -69,7 +107,7 @@ Each ends with `pnpm harness && pnpm check-types && pnpm build`. exists. Routes that rely on test verifiers are refused outside test networks; don't try to loosen that. ### Features: swaps, on-ramps, prices -Partner keys go in `packages/extension/.env` (see `.env.example`), read at build time: `CLIP_0X_API_KEY`, +Partner keys go in `.env` at the project root (see `.env.example`), read at build time: `CLIP_0X_API_KEY`, `CLIP_JUPITER_API_KEY`, `CLIP_COINGECKO_DEMO_KEY`, `CLIP_MOONPAY_API_KEY` + `CLIP_MOONPAY_SIGNER_URL`, `CLIP_BANXA_PARTNER`, `CLIP_C14_CLIENT_ID` + `CLIP_C14_ASSET_IDS`. Without a key the feature says it isn't switched on. Staking and the swaps that need no key (SaucerSwap, Minswap, STON.fi, …) work as they are. @@ -100,16 +138,23 @@ this project needs to change to allow them. ### Hardware wallets `hardware: ["ledger", "keystone"]` in `clip.config.ts`; remove one to hide it. + ### Change the dapp 1. Pages: `packages/nextjs/app//page.tsx`; add a link in `components/Header.tsx`. 2. The wallet's name and rdns come from `utils/wallet.ts` (which reads `wallet.identity.json`); don't hard-code them. 3. A contract on the debug page: add `{ address, abi }` under chain `296` in `contracts/externalContracts.ts`. 4. Own contracts: add Scaffold-HBAR's `packages/foundry` (or hardhat); its deploy fills `contracts/deployedContracts.ts`. 5. Testnet only: `scaffold.config.ts` lists `hederaTestnet`; add `hedera` only after the wallet's mainnet checklist. + + +### Ship: stores and signing +`docs/signing.md` lists the accounts and certificates each store needs and the environment variable names the tools read +(Chrome Web Store, AMO, Edge; Apple Developer ID and notarization; Windows code signing; App Store and Google Play via +EAS). Never add a certificate, keystore or credential to this repository; never sign or submit for the owner. ### Upgrade the kit -1. Change the exact version of `@clip-wallet/config` and `@clip-wallet/extension-kit` in `packages/extension/package.json` - and of `create-clip-wallet` in the root `package.json` (all the same version). +1. Change the exact version of every `@clip-wallet/*` package in `packages/*/package.json` and of `@clip-wallet/config` + and `create-clip-wallet` in the root `package.json` (all the same version). 2. `pnpm install && pnpm verify:provenance && pnpm harness && pnpm check-types && pnpm build`. 3. Read the kit's changelog for anything that needs a config change. diff --git a/templates/scaffold-hbar-clip-wallet/packages/extension/MAINNET.md b/templates/scaffold-hbar-clip-wallet/MAINNET.md similarity index 70% rename from templates/scaffold-hbar-clip-wallet/packages/extension/MAINNET.md rename to templates/scaffold-hbar-clip-wallet/MAINNET.md index 89da30a..31aa382 100644 --- a/templates/scaffold-hbar-clip-wallet/packages/extension/MAINNET.md +++ b/templates/scaffold-hbar-clip-wallet/MAINNET.md @@ -12,15 +12,16 @@ mainnet: { enabled: true, acknowledged: MAINNET_ACKNOWLEDGEMENT }, Three things enforce it, so it can't happen by accident: - `pnpm harness` fails while mainnet is on and any box here is still `[ ]`; -- the extension build (`clipWallet()` from @clip-wallet/extension-kit) refuses a mainnet config while any box here is - open, or with a placeholder rdns, no homepage, no extension key, no WalletConnect project id or a remote icon; +- every build (`clipWallet()`, `clipDesktop()`, `expoConfig()` / `withClipWallet()` from the kit) refuses a mainnet + config while any box here is open, or with a placeholder rdns, no homepage, no extension key, no WalletConnect project + id or a remote icon; - `pnpm wallet:mainnet-check` lists everything that is left (exit 0 only when nothing is). Coding agents: never tick these boxes or turn mainnet on. Ask the owner. ## Identity -- [ ] `wallet.identity.json` has your own name, a reverse domain you own as `rdns`, your `homepage` and a description (`pnpm wallet:identity`). -- [ ] The icon is your artwork: `icon.svg` (or `icon.png`) and `public/icon/16.png`, `32.png`, `48.png`, `128.png`. +- [ ] `wallet.identity.json` has your own name, a reverse domain you own as `rdns`, your `homepage`, a description and the app id your stores know (`pnpm wallet:identity`). +- [ ] The icon is your artwork: `icon.svg` (or `icon.png`), and every platform's icons were rendered from it (`pnpm wallet:brand`). - [ ] The extension's private key (`.keys/extension.pem`) is backed up offline, and the Chrome Web Store item uses the same key (its id matches `docs/listings/README.md`). ## Keys and services @@ -32,6 +33,7 @@ Coding agents: never tick these boxes or turn mainnet on. Ask the owner. - [ ] The `@clip-wallet/*` packages are pinned to one release and `pnpm verify:provenance` passes for it. - [ ] Every change you made on top of the kit has been reviewed by someone other than its author (the kit's own audits don't cover your edits). - [ ] `pnpm harness`, `pnpm check-types` and `pnpm build` pass on the release commit, in CI. +- [ ] Every build you ship is signed by you (docs/signing.md): the store-signed extension, Developer ID + notarized macOS and code-signed Windows apps, and store-signed iOS / Android apps. ## People - [ ] Your homepage says who runs the wallet, that it is non-custodial (the recovery phrase never leaves the device; nobody can recover it for the user), and how to get support. @@ -39,5 +41,7 @@ Coding agents: never tick these boxes or turn mainnet on. Ask the owner. - [ ] The three usability tasks in `.harness/prd.md` passed with five people on testnet, with no wrong-network mistakes. - [ ] Store listings have a privacy policy and terms that match what the wallet sends where (Settings → Security lists it). -When the last box is ticked: set `mainnet` as above, add `chains.hedera` to `packages/nextjs/scaffold.config.ts` if the -demo dapp should follow, run `pnpm wallet:mainnet-check`, and only then build for the stores. +When the last box is ticked: set `mainnet` as above, run `pnpm wallet:mainnet-check`, and only then build for the stores. + +If the demo dapp should follow, add `chains.hedera` to `packages/nextjs/scaffold.config.ts`. + diff --git a/templates/scaffold-hbar-clip-wallet/NOTICE b/templates/scaffold-hbar-clip-wallet/NOTICE index d799ef7..5092610 100644 --- a/templates/scaffold-hbar-clip-wallet/NOTICE +++ b/templates/scaffold-hbar-clip-wallet/NOTICE @@ -6,10 +6,11 @@ Licensed under the Apache License, Version 2.0 (see LICENCE). "Clip Wallet", "1Mask" and the Clip Wallet logo are trademarks of ColdAI. The Apache License 2.0 grants no rights to use them (Section 6). A wallet made from this template -uses its own name and icon (packages/extension/wallet.identity.json). +uses its own name and icon (wallet.identity.json). -packages/nextjs/ is based on Scaffold-HBAR (https://github.com/hedera-dev/scaffold-hbar) -and Scaffold-ETH 2 (https://github.com/scaffold-eth/scaffold-eth-2), MIT licence, -Copyright (c) 2023 BuidlGuidl and Copyright (c) 2026 hedera-dev; see -packages/nextjs/LICENSE. The Hedera logos in packages/nextjs/public/ are trademarks of -Hedera Hashgraph, LLC and are not covered by either licence. +packages/nextjs/, the Scaffold-HBAR dapp in projects that include it, is based on +Scaffold-HBAR (https://github.com/hedera-dev/scaffold-hbar) and Scaffold-ETH 2 +(https://github.com/scaffold-eth/scaffold-eth-2), MIT licence, Copyright (c) 2023 +BuidlGuidl and Copyright (c) 2026 hedera-dev; see packages/nextjs/LICENSE. The Hedera +logos in packages/nextjs/public/ are trademarks of Hedera Hashgraph, LLC and are not +covered by either licence. diff --git a/templates/scaffold-hbar-clip-wallet/README.md b/templates/scaffold-hbar-clip-wallet/README.md index 173adb8..08a3f91 100644 --- a/templates/scaffold-hbar-clip-wallet/README.md +++ b/templates/scaffold-hbar-clip-wallet/README.md @@ -1,25 +1,31 @@
-# Scaffold-HBAR × Clip Wallet +# Your wallet, on every platform -**Your own non-custodial wallet, plus a Hedera dapp that connects to it. One command.** +**A non-custodial wallet under your own name: browser extension, desktop app and phone app, from one `clip.config.ts`.** -A [Scaffold-HBAR](https://github.com/hedera-dev/scaffold-hbar) template by [ColdAI](https://coldai.org), built on the -[Clip Wallet kit](https://github.com/ColdAI-org/clip-wallet). +Built on the [Clip Wallet kit](https://github.com/ColdAI-org/clip-wallet) by [ColdAI](https://coldai.org). + +A [Scaffold-HBAR](https://github.com/hedera-dev/scaffold-hbar) template (**Scaffold-HBAR × Clip Wallet**): your wallet +plus a Hedera dapp that connects to it. [![CI](https://github.com/ColdAI-org/scaffold-hbar-clip-wallet/actions/workflows/ci.yaml/badge.svg)](https://github.com/ColdAI-org/scaffold-hbar-clip-wallet/actions/workflows/ci.yaml) [![Fresh scaffold](https://github.com/ColdAI-org/scaffold-hbar-clip-wallet/actions/workflows/fresh-scaffold.yaml/badge.svg)](https://github.com/ColdAI-org/scaffold-hbar-clip-wallet/actions/workflows/fresh-scaffold.yaml) -[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENCE) [![Hedera testnet](https://img.shields.io/badge/Hedera-testnet-8259EF?logo=hedera)](#the-demo-dapp) + +[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENCE) [![npm provenance](https://img.shields.io/badge/npm-provenance-2ea043)](#signed-versioned-kit)
```bash -npm create scaffold-hbar@latest -- --template ColdAI-org/scaffold-hbar-clip-wallet -# or, the same project in one step: -npx create-clip-wallet my-wallet +npx create-clip-wallet my-wallet # asks what it needs; --yes for the defaults +npx create-clip-wallet my-wallet --platforms extension,desktop --scaffold-hbar --name "Acme Wallet" --yes ``` + + +The same project through Scaffold-HBAR: `npm create scaffold-hbar@latest -- --template ColdAI-org/scaffold-hbar-clip-wallet`. + --- @@ -27,35 +33,95 @@ npx create-clip-wallet my-wallet | | | | |---|---|---| -| 🧩 | **A real wallet, under your name** | A Manifest V3 browser extension with its own name, icon, extension id, EIP-6963 rdns and WalletConnect project. It never announces itself as Clip Wallet. | +| 🧩 | **A real wallet, under your name** | Its own name, icons, app ids, extension id, EIP-6963 rdns, deep-link scheme and WalletConnect project on every platform. It never announces itself as Clip Wallet. | +| 🖥️ | **Every platform from one config** | A Manifest V3 browser extension, a desktop app for macOS, Windows and Linux (Electron), and a phone app for iOS and Android (Expo). `clip.config.ts` drives all of them. | | 🌐 | **14 network families** | EVM (any chain id), Hedera, Solana, Bitcoin, Sui, Aptos, Cardano, Polkadot SDK, Starknet, TON, NEAR, Stellar, Tezos, Algorand. Turn them on and off in `clip.config.ts`. | -| 🔌 | **1Mask: every dapp works** | EIP-1193 + EIP-6963, Wallet Standard, AIP-62, CIP-30, injectedWeb3, get-starknet, TON Connect, NEAR Connect, SEP-43, Beacon and WalletConnect, all announced with your identity. | +| 🔌 | **1Mask: every dapp works** | EIP-1193 + EIP-6963, Wallet Standard, AIP-62, CIP-30, injectedWeb3, get-starknet, TON Connect, NEAR Connect, SEP-43, Beacon and WalletConnect, all announced with your identity (in the extension and in the desktop and phone apps' built-in browsers). | | 🧾 | **Every request in plain words** | Each dapp request is decoded before approval (balance changes, fee, warnings); blind signing is off by default. | | 🛡️ | **A security floor you can't switch off** | Open phishing lists, address-poisoning and new-contract checks, permission review and revoke, spam cleanup. Blockaid scanning when you add a key. | | ✨ | **Features, social, plugins, settle** | Staking and swaps per family, on-ramps, Secure Trade on Hedera, contacts and Clip handles, notifications, Discover, sandboxed Clip Plugins, route-and-fund and settle-on-Hedera quotes on CLPRouter. | -| 🧪 | **A Scaffold-HBAR dapp** | Next.js on Hedera testnet: connect your wallet through 1Mask, sign, send HBAR, and call Hedera system contracts from the debug page. | -| 🔏 | **Testnet by default** | Mainnet needs a checklist a person ticks (`packages/extension/MAINNET.md`); the build and the harness refuse it otherwise. | +| 🧪 | **A Scaffold-HBAR dapp** | Next.js on Hedera testnet: connect your wallet through 1Mask, sign, send HBAR, and call Hedera system contracts from the debug page. | +| 🔏 | **Testnet by default** | Mainnet needs a checklist a person ticks (`MAINNET.md`); every build and the harness refuse it otherwise. | | 🤖 | **Agent-friendly** | `AGENTS.md` recipes, `llms.txt`, a typed `clip.config.ts`, and `pnpm harness` validators an agent must pass. | ## Quick start -Prerequisites: Node **22+**, [pnpm](https://pnpm.io) **10+**, Git, Chrome or another Chromium browser. +Prerequisites: Node **22.18+**, [pnpm](https://pnpm.io) **10+**, Git. Chrome or another Chromium browser for the +extension; nothing else for the desktop app; for the phone app, Xcode (iOS) or Android Studio (Android) to run a +development build, or nothing at all to export its JavaScript bundles. ```bash -npm create scaffold-hbar@latest -- --template ColdAI-org/scaffold-hbar-clip-wallet cd pnpm install -pnpm wallet:identity --name "Acme Wallet" --rdns com.acme.wallet # your identity (see below) -pnpm extension:build # → packages/extension/.output/chrome-mv3 -pnpm next:dev # → http://localhost:3000 +pnpm harness # the rules (below); must pass before every commit +``` + + +**Browser extension** (`packages/extension`) + +```bash +pnpm dev:extension # Chrome with the extension loaded, live reload +pnpm extension:build # → packages/extension/.output/chrome-mv3: Load unpacked in chrome://extensions +pnpm extension:zip # the store upload zip ``` -Load `packages/extension/.output/chrome-mv3` with **Load unpacked** in `chrome://extensions` (Developer mode on), -create a wallet in it, get testnet HBAR from the [faucet](https://portal.hedera.com/faucet), and open the dapp. + + +**Desktop app: macOS, Windows, Linux** (`packages/desktop`) + +```bash +pnpm dev:desktop # run it with live reload +pnpm desktop:dist # installers for this computer → packages/desktop/release (unsigned) +pnpm desktop:dist:mac # dmg + zip · desktop:dist:win NSIS + zip · desktop:dist:linux AppImage + deb + tar.gz +``` + + + +**Phone app: iOS, Android** (`packages/mobile`) + +```bash +pnpm --filter mobile start # Metro for a development build (expo-dev-client) +pnpm mobile:prebuild # generate ios/ and android/ (never committed), then: +pnpm --filter mobile ios # build and run on the iOS simulator · pnpm --filter mobile android +pnpm mobile:export # the JavaScript bundles for iOS and Android: no account, no Xcode +``` -This template manages its own package manager (pnpm), so create-scaffold-hbar skips the install step and prints these -commands. `npx create-clip-wallet my-wallet` does the copy and the identity step in one go and asks for anything you -don't pass as flags; both produce the same project. + + +**The demo dapp** (`packages/nextjs`) + +```bash +pnpm next:dev # → http://localhost:3000 +``` + +Load the extension, create a wallet in it, get testnet HBAR from the [faucet](https://portal.hedera.com/faucet), and +open the dapp. Through Scaffold-HBAR, run `pnpm wallet:identity --name "Acme Wallet" --rdns com.acme.wallet` first: +this template manages its own package manager (pnpm), so create-scaffold-hbar skips the install step and prints these +commands. `npx create-clip-wallet my-wallet --scaffold-hbar` does the copy and the identity step in one go; both +produce the same project. + + +Store accounts and code signing (Chrome Web Store, Apple Developer ID and notarization, Windows certificates, App Store, +Google Play) are only needed to ship: [docs/signing.md](docs/signing.md) lists them and the variables the tools read. + +## One config, every platform + +`clip.config.ts` (typed by `@clip-wallet/config`) and `wallet.identity.json` next to it are the only places the wallet +is described: + +| Setting | Extension | Desktop | Phone | +|---|---|---|---| +| `name`, `description` | manifest, pickers | window titles, menus, installers | app name, permission texts | +| `rdns` (EIP-6963) | announced to dapps | announced in the built-in browser | announced in the in-app browser | +| `appId` (default: `rdns`) | | `.desktop` (macOS bundle id, Windows AppUserModelID) | iOS bundle id, Android package | +| `icon` (`icon.svg` / `icon.png`) | toolbar icons | `.icns`, `.ico`, Linux PNGs, tray | icon, adaptive icon, splash | +| `theme` | screens | screens | screens, adaptive-icon and splash background | +| `networks`, `languages` | ✓ | ✓ | ✓ | +| `scheme` (default: the name, lower case) | | `://wc?uri=…` | `://wc?uri=…` | +| `services` | backup, media proxy, relay, Clip handles | same | same | +| `mainnet` | refused until `MAINNET.md` is done | same | same | + +`fees` and `usage` are reserved for Clip Cloud's hosted mode: accepted, off by default, and never acted on by the kit. ## Your wallet's identity @@ -63,22 +129,26 @@ don't pass as flags; both produce the same project. | | Where | | |---|---|---| -| Name, description, homepage | `packages/extension/wallet.identity.json` | Manifest, wallet pickers, WalletConnect metadata | +| Name, description, homepage, app id | `wallet.identity.json` | Every platform's name and ids, wallet pickers, WalletConnect metadata | | EIP-6963 rdns | same file | A reverse domain you own; dapps and wallet lists key on it | -| Extension id | `extension.key` in the same file | An RSA public key that fixes the Chrome extension id; the private key goes to `packages/extension/.keys/extension.pem` (gitignored, 0600) | -| Icon | `packages/extension/icon.svg`, `public/icon/*.png` | A starter mark in your accent colour; replace with your art (`--icon logo.svg`) | -| WalletConnect project id | `packages/extension/.env`, `packages/nextjs/.env.local` | `--walletconnect-project-id`; never committed | +| Extension id | `extension.key` in the same file | An RSA public key that fixes the Chrome extension id; the private key goes to `.keys/extension.pem` (gitignored, 0600) | +| Icons | `icon.svg` (or `icon.png`) and every platform's icon files | A starter mark in your accent colour; `--logo logo.png` renders every size from your logo (`pnpm wallet:brand`) | +| WalletConnect project id | `.env` (and the dapp's `.env.local`, if there is one) | `--walletconnect-project-id`; never committed | | Window global, TON Connect key | derived from the name | `window.acmewallet.near`, TON Connect `acmewallet` | | Listing drafts | `docs/listings/` | TON Connect, NEAR, Stellar Wallets Kit, Beacon, use-wallet, WalletConnect Explorer, EIP-6963 | ```bash -pnpm wallet:identity --name "Acme Wallet" --rdns com.acme.wallet --homepage https://wallet.acme.com \ - --accent "#0B7A3B" --icon ./acme.svg --walletconnect-project-id <32 hex> -pnpm wallet:listings # regenerate docs/listings after an identity change +pnpm wallet:identity --name "Acme Wallet" --rdns com.acme.wallet --id com.acme.wallet --homepage https://wallet.acme.com \ + --accent "#0B7A3B" --logo ./acme.png --walletconnect-project-id <32 hex> +pnpm wallet:brand --logo ./acme-v2.png # every platform's icons again, from a new logo +pnpm wallet:listings # regenerate docs/listings after an identity change ``` -Running it again keeps the extension key (and so the id) unless you pass `--new-key`. +Running it again keeps the extension key (and so the id) unless you pass `--new-key`. Icons are rendered with Node +alone from a PNG logo (1024×1024 is best); an SVG logo is rasterised once with whichever of `@resvg/resvg-js`, +`rsvg-convert`, `sips` (macOS) or ImageMagick is installed. + ## The demo dapp `packages/nextjs` is Scaffold-HBAR's Next.js app on Hedera testnet (chain 296): @@ -92,16 +162,21 @@ Running it again keeps the extension key (and so the id) unless you pass `--new- Every request opens your wallet's approval window, decoded in plain words. The header's **Connect Wallet** (RainbowKit) lists your wallet next to MetaMask and WalletConnect. + ## Commands | Command | | |---|---| -| `pnpm extension:build` / `extension:dev` / `extension:zip` | Build, watch, or zip the extension for the stores | -| `pnpm extension:build:fixtures` | The extension with sample data and no network, for screenshots | -| `pnpm next:dev` / `next:build` / `next:serve` | The demo dapp | +| `pnpm dev:extension` · `extension:build` · `extension:zip` | Watch, build, or zip the extension for the stores | +| `pnpm extension:build:fixtures` | The extension with sample data and no network, for screenshots | +| `pnpm dev:desktop` · `desktop:build` · `desktop:start` | Run, build, or start the built desktop app | +| `pnpm desktop:dist` · `desktop:dist:mac` · `:win` · `:linux` | Installers (electron-builder), unsigned unless docs/signing.md is set up | +| `pnpm --filter mobile start` · `ios` · `android` | Metro, or build and run a development build | +| `pnpm mobile:prebuild` · `mobile:export` | Native projects (generated) · JavaScript bundles | +| `pnpm next:dev` / `next:build` / `next:serve` | The demo dapp | | `pnpm harness` | The rules (below). Must pass before every commit | -| `pnpm check-types` · `pnpm build` · `pnpm lint` | Types, both builds, lint | -| `pnpm wallet:identity` · `wallet:listings` · `wallet:mainnet-check` | Identity, listing drafts, what blocks mainnet | +| `pnpm check-types` · `pnpm build` | Types for every package, the builds | +| `pnpm wallet:identity` · `wallet:brand` · `wallet:listings` · `wallet:mainnet-check` | Identity, icons, listing drafts, what blocks mainnet | | `pnpm verify:provenance` | Check the kit packages were built by the kit's CI from its public repo | ## The rules (`pnpm harness`) @@ -111,50 +186,62 @@ lists your wallet next to MetaMask and WalletConnect. - key material is handled outside `@clip-wallet/vault`, a secret is logged, a recovery phrase is committed, or a `.env` or key file is tracked by git; - the wallet announces Clip Wallet's identity (`org.coldai.*` or the name "Clip Wallet"); -- `wxt.config.ts` stops building through `clipWallet()`, or anything switches the phishing lists off; -- mainnet is on while `packages/extension/MAINNET.md` has an open box; +- a platform stops building through its kit (`clipWallet()`, `clipDesktop()`, `expoConfig()` / `withClipWallet()`), or + anything switches the phishing lists off; +- mainnet is on while `MAINNET.md` has an open box; - a kit package isn't pinned to one exact version. It warns while the template's placeholder identity is still in place. ## Signed, versioned kit -The wallet itself is `@clip-wallet/extension-kit` and the `@clip-wallet/*` packages it depends on, all released together -with one version and published by the kit's CI with **npm provenance** (Sigstore-signed, traceable to the commit and -workflow). This project pins that version exactly (the harness insists); `pnpm verify:provenance` checks every package's -attestation against `github.com/ColdAI-org/clip-wallet`, and `npm audit signatures` verifies the signatures in an npm -install. Upgrading is a deliberate change: bump the pin in `packages/extension/package.json` and the root -`create-clip-wallet`, read the kit's changelog, run `pnpm install && pnpm verify:provenance && pnpm harness && pnpm build`. +The wallet itself is `@clip-wallet/extension-kit`, `@clip-wallet/desktop-kit` and `@clip-wallet/mobile-kit` and the +`@clip-wallet/*` packages they depend on, all released together with one version and published by the kit's CI with +**npm provenance** (Sigstore-signed, traceable to the commit and workflow). This project pins that version exactly (the +harness insists); `pnpm verify:provenance` checks every package's attestation against `github.com/ColdAI-org/clip-wallet`, +and `npm audit signatures` verifies the signatures in an npm install. Upgrading is a deliberate change: bump the pins in +`packages/*/package.json` and the root `package.json`, read the kit's changelog, run +`pnpm install && pnpm verify:provenance && pnpm harness && pnpm build`. ## Testnet first -Everything runs on test networks. `packages/extension/MAINNET.md` lists what a person must do first (own rdns and -homepage, the extension key backed up and matching the store item, your own WalletConnect project, production services, -an independent review of your changes, usability tests, support and security contacts). The build and the harness both -refuse a mainnet config while a box is open, and the build also until `pnpm wallet:mainnet-check` is clean. +Everything runs on test networks. `MAINNET.md` lists what a person must do first (own rdns and homepage, the extension +key backed up and matching the store item, your own WalletConnect project, production services, signed builds, an +independent review of your changes, usability tests, support and security contacts). Every build and the harness refuse +a mainnet config while a box is open, and the builds also until `pnpm wallet:mainnet-check` is clean. ## Project layout -``` -packages/extension/ your wallet: wallet.identity.json, clip.config.ts, icon, one-line WXT entrypoints -packages/nextjs/ the Scaffold-HBAR dapp: demo page, /debug, Hedera testnet -docs/listings/ listing-submission drafts for your identity -tools/harness/check.mjs the rules above -tools/verify-provenance.mjs provenance check for the kit packages -.harness/ spec, PRD and hedera-harness validators for coding agents -AGENTS.md · llms.txt how agents (and people) change this project safely -``` +| Path | | +|---|---| +| `clip.config.ts` · `wallet.identity.json` | The wallet: identity, theme, networks, languages, services, mainnet (every platform) | +| `icon.svg` | The logo (or `icon.png`); every platform's icons are rendered from it | +| `MAINNET.md` | The owner's mainnet checklist | +| `packages/extension/` | The browser extension: one-line WXT entrypoints on `@clip-wallet/extension-kit` | +| `packages/desktop/` | The desktop app: one-line electron-vite entrypoints on `@clip-wallet/desktop-kit`, icons | +| `packages/mobile/` | The phone app: Expo on `@clip-wallet/mobile-kit`, icons and splash | +| `packages/nextjs/` | The Scaffold-HBAR dapp: demo page, /debug, Hedera testnet | +| `docs/listings/` | Listing-submission drafts for your identity | +| `docs/signing.md` | Code signing and store accounts (placeholders: what to set, never secrets) | +| `tools/harness/check.mjs` | The rules above | +| `tools/verify-provenance.mjs` | Provenance check for the kit packages | +| `.harness/` | Spec, PRD and hedera-harness validators for coding agents | +| `AGENTS.md` · `llms.txt` | How agents (and people) change this project safely | ## For coding agents [AGENTS.md](AGENTS.md) is written for Claude Code, Codex, Cursor and friends: the rules that never break, the commands -that must pass, and recipes for identity, networks, tokens, routing and settle-on-Hedera, features, security, social, -plugins and the dapp. [`.harness/`](.harness/README.md) holds the spec, the PRD and a hedera-harness recipe with +that must pass, and recipes for identity, platforms, networks, tokens, routing and settle-on-Hedera, features, security, +social, plugins and the dapp. [`.harness/`](.harness/README.md) holds the spec, the PRD and a hedera-harness recipe with validators. ## License -Apache-2.0, see [LICENCE](LICENCE) and [NOTICE](NOTICE). Created by [ColdAI](https://coldai.org). The dapp -(`packages/nextjs`) builds on Scaffold-HBAR (hedera-dev) and Scaffold-ETH 2 (BuidlGuidl) and keeps their MIT licence -([`packages/nextjs/LICENSE`](packages/nextjs/LICENSE)); the wallet is the Clip Wallet kit. "Clip Wallet", "1Mask" and -the Clip Wallet logo are ColdAI trademarks and aren't licensed: your wallet uses its own name and icon. +Apache-2.0, see [LICENCE](LICENCE) and [NOTICE](NOTICE). Created by [ColdAI](https://coldai.org). The wallet is the +Clip Wallet kit. "Clip Wallet", "1Mask" and the Clip Wallet logo are ColdAI trademarks and aren't licensed: your wallet +uses its own name and icon. + + +The dapp (`packages/nextjs`) builds on Scaffold-HBAR (hedera-dev) and Scaffold-ETH 2 (BuidlGuidl) and keeps their MIT +licence ([`packages/nextjs/LICENSE`](packages/nextjs/LICENSE)). + diff --git a/templates/scaffold-hbar-clip-wallet/packages/extension/clip.config.ts b/templates/scaffold-hbar-clip-wallet/clip.config.ts similarity index 52% rename from templates/scaffold-hbar-clip-wallet/packages/extension/clip.config.ts rename to templates/scaffold-hbar-clip-wallet/clip.config.ts index 32b7cc6..fc3c80c 100644 --- a/templates/scaffold-hbar-clip-wallet/packages/extension/clip.config.ts +++ b/templates/scaffold-hbar-clip-wallet/clip.config.ts @@ -1,28 +1,34 @@ import { defineConfig } from "@clip-wallet/config"; -// Name, description, rdns, homepage, icon and the extension's public key. `pnpm wallet:identity` writes this file; -// edit it there, not here. +// Name, description, rdns, homepage, icon, app id and the extension's public key. `pnpm wallet:identity` writes this +// file; edit it there, not here. import identity from "./wallet.identity.json" with { type: "json" }; /** - * Everything about this wallet that isn't identity. Each setting and its default is documented in - * @clip-wallet/config (node_modules/@clip-wallet/config/README.md); invalid values fail the build in plain words. + * The one config for every platform this wallet ships: the browser extension (packages/extension), the desktop app + * (packages/desktop) and the phone app (packages/mobile). Each setting and its default is documented in + * @clip-wallet/config (node_modules/@clip-wallet/config/README.md); invalid values fail every build in plain words. */ export default defineConfig({ ...identity, - // Buttons use the accent with accentText on top: keep a contrast of at least 3:1. + // Buttons use the accent with accentText on top: keep a contrast of at least 3:1. The accent is also the phone + // app's adaptive-icon and splash background. theme: { accent: "#4F46E5", accentText: "#FFFFFF", font: "Inter", radius: 12 }, // "evm:*" or "evm:", hedera, solana, bitcoin, sui, aptos, cardano, substrate, starknet, ton, near, stellar, // tezos, algorand. Test networks only while mainnet is false. networks: ["evm:*", "hedera", "solana", "bitcoin"], + // Settings → Language offers these (en, de, fr, es, pt-BR, it, tr, ja, ko, zh-Hans, ar, hi); the first is the fallback. + languages: ["en", "de", "fr", "es", "pt-BR", "it", "tr", "ja", "ko", "zh-Hans", "ar", "hi"], + // Deep links (://wc?uri=…) on desktop and phone. Default: the name in lower case without spaces. + // scheme: "mywallet", // Route and fund on CLPRouter. settleOnHedera: Phase 3 bonded-Connector quotes in the approval's Details. route: { mode: "balanced", filters: {}, settleOnHedera: false }, hardware: ["ledger", "keystone"], // The WalletConnect project id comes from CLIP_WALLETCONNECT_PROJECT_ID (.env), never from this file. passkeys: { enabled: true }, - // Optional hosted services (backup, NFT media proxy, Clip handles). Unset = those features stay hidden. + // Optional hosted services (backup, NFT media proxy, phone-as-signer relay, Clip handles). Unset = hidden. services: {}, // Test networks only. Mainnet needs every box in MAINNET.md ticked, then // mainnet: { enabled: true, acknowledged: MAINNET_ACKNOWLEDGEMENT } - // and the build refuses it until `pnpm wallet:mainnet-check` is clean. + // and every build refuses it until `pnpm wallet:mainnet-check` is clean. mainnet: false, }); diff --git a/templates/scaffold-hbar-clip-wallet/docs/signing.md b/templates/scaffold-hbar-clip-wallet/docs/signing.md new file mode 100644 index 0000000..6ce99e8 --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/docs/signing.md @@ -0,0 +1,88 @@ +# Code signing and store setup + +Nothing in this project is signed, and no account is needed to build it: the extension zip, the desktop installers +(unsigned) and the phone app's JavaScript bundles and native projects all build on a fresh checkout. Shipping to people +needs the accounts and certificates below. This page lists **what each one is and the environment variable names the +tools read**. Put the values in your CI's secret store (or a local `.env` that is never committed); never in this +repository, never in `clip.config.ts`. + +| Platform | Account | Certificate / key | Read by | +| --- | --- | --- | --- | +| Chrome Web Store | Google developer account (one-time fee) | the extension key in `.keys/extension.pem` (fixes the extension id) | `wxt submit` | +| Firefox Add-ons | addons.mozilla.org account | AMO API key and secret | `wxt submit` | +| Microsoft Edge Add-ons | Partner Center account | Edge API credentials | `wxt submit` | +| macOS | Apple Developer Program | Developer ID Application certificate; notarization API key | electron-builder | +| Windows | a code-signing certificate authority (OV or EV), or Azure Trusted Signing | the certificate (.pfx) or Azure credentials | electron-builder | +| Linux | none (signing optional) | optional GPG key for your own apt repository | your packaging | +| iOS | Apple Developer Program, App Store Connect | distribution certificate + provisioning profile (EAS can manage them) | EAS / Xcode | +| Android | Google Play Console | upload keystore (EAS can manage it); Play service-account key for uploads | EAS / Gradle | + + +## Browser extension + +```sh +pnpm extension:zip # packages/extension/.output/*.zip for the stores +``` + +Keep `.keys/extension.pem` backed up offline: the Chrome Web Store item must use the same key, or the extension id +(and every listing that names it, `docs/listings/`) changes. Store uploads from CI (`wxt submit`) read: + +- Chrome: `CHROME_EXTENSION_ID`, `CHROME_CLIENT_ID`, `CHROME_CLIENT_SECRET`, `CHROME_REFRESH_TOKEN` +- Firefox: `FIREFOX_EXTENSION_ID` (your gecko id, `wallet@`), `FIREFOX_JWT_ISSUER`, `FIREFOX_JWT_SECRET` +- Edge: `EDGE_PRODUCT_ID`, `EDGE_CLIENT_ID`, `EDGE_API_KEY` + + + +## Desktop (macOS, Windows, Linux) + +```sh +pnpm desktop:dist # installers for this computer's OS → packages/desktop/release/ +pnpm desktop:dist:mac # dmg + zip, arm64 and x64 +pnpm desktop:dist:win # NSIS installer + zip (NSIS needs Windows or CI) +pnpm desktop:dist:linux # AppImage + deb (Linux or CI) + tar.gz (anywhere) +``` + +Without the variables below the builds are unsigned: fine for testing, not for people (macOS Gatekeeper blocks +unsigned apps; Windows SmartScreen warns). + +- **macOS signing** (Developer ID Application, exported as .p12): `CSC_LINK` (file path or base64), `CSC_KEY_PASSWORD`. + Or `CSC_NAME` for an identity already in the build machine's keychain. +- **macOS notarization**: an App Store Connect API key: `APPLE_API_KEY` (path to the .p8), `APPLE_API_KEY_ID`, + `APPLE_API_ISSUER`. Or an Apple ID: `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`. +- **Windows signing**: `WIN_CSC_LINK` (.pfx path or base64), `WIN_CSC_KEY_PASSWORD`. EV certificates on a hardware + token, or Azure Trusted Signing, need electron-builder's `win.signtoolOptions` / `win.azureSignOptions` (add them over + `electronBuilderConfig()` in `packages/desktop/electron-builder.config.cjs`; Azure reads `AZURE_TENANT_ID`, + `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`). +- **Auto-updates** (off by default; signed builds only): `CLIP_UPDATES=1` with `CLIP_UPDATES_OWNER` and + `CLIP_UPDATES_REPO` (a GitHub repository for releases) at build time, and `GH_TOKEN` to publish. +- **Your extension talking to the desktop app** (native messaging): `CLIP_EXTENSION_IDS` lists your extension's + Chromium ids at build time. + +The app id (`.desktop`), product name and deep-link scheme come from `clip.config.ts` / `wallet.identity.json`. + + + +## Phone (iOS, Android) + +```sh +pnpm mobile:export # JavaScript bundles for iOS and Android (no account) +pnpm mobile:prebuild # packages/mobile/ios and android (generated, never committed) +pnpm --filter mobile eas:build # cloud builds with EAS (needs EXPO_TOKEN) +``` + +- **Expo / EAS** (optional; you can build `ios/` in Xcode and `android/` in Android Studio instead): an Expo account and + `EXPO_TOKEN` in CI. `packages/mobile/eas.json` has development, preview and production profiles. +- **iOS**: an Apple Developer Program membership, the bundle id (`mobile.bundleId` or `appId` in clip.config) registered + in your developer account, a distribution certificate and provisioning profile (EAS creates and stores them when you + let it). App Store uploads (`eas submit`) read an App Store Connect API key: `EXPO_ASC_API_KEY_PATH`, + `EXPO_ASC_API_KEY_ID`, `EXPO_ASC_API_KEY_ISSUER_ID`, plus `EXPO_APPLE_TEAM_ID`. +- **Android**: a Google Play Console account and an upload keystore (EAS can generate and keep it; if you build with + Gradle yourself, keep the keystore and its passwords out of git and pass them to Gradle from your CI's secrets). Play + uploads use a Google Cloud service-account JSON key: point `submit.production.android.serviceAccountKeyPath` in + `eas.json` at a file your CI writes from a secret; never commit the key. +- **Universal links and passkeys**: `CLIP_ASSOCIATED_DOMAIN` (your domain) at build time, plus the domain's + `/.well-known/apple-app-site-association` and `/.well-known/assetlinks.json` files. Needs signed builds. + + +Store listings also need a privacy policy, a support contact and screenshots; the stores' data-safety forms must match +what Settings → Security says the wallet sends where. diff --git a/templates/scaffold-hbar-clip-wallet/packages/extension/icon.svg b/templates/scaffold-hbar-clip-wallet/icon.svg similarity index 100% rename from templates/scaffold-hbar-clip-wallet/packages/extension/icon.svg rename to templates/scaffold-hbar-clip-wallet/icon.svg diff --git a/templates/scaffold-hbar-clip-wallet/llms.txt b/templates/scaffold-hbar-clip-wallet/llms.txt index 8c84578..93a3505 100644 --- a/templates/scaffold-hbar-clip-wallet/llms.txt +++ b/templates/scaffold-hbar-clip-wallet/llms.txt @@ -1,31 +1,64 @@ -# Scaffold-HBAR × Clip Wallet +# A wallet on the Clip Wallet kit (Scaffold-HBAR × Clip Wallet) -> A non-custodial browser wallet with its own identity, built on the Clip Wallet kit (@clip-wallet/extension-kit), plus a Scaffold-HBAR Next.js dapp that connects to it on Hedera testnet. pnpm workspace, TypeScript, Apache-2.0. Test networks by default. +> A non-custodial wallet with its own identity on every platform it ships (browser extension, desktop app, phone app), all from one clip.config.ts, built on the Clip Wallet kit (@clip-wallet/extension-kit, desktop-kit, mobile-kit). pnpm workspace, TypeScript, Apache-2.0. Test networks by default. -Read first: AGENTS.md (rules + recipes), .harness/spec.md (product), .harness/prd.md (stories), packages/extension/clip.config.ts (typed config), packages/extension/wallet.identity.json (identity). +Read first: AGENTS.md (rules + recipes), .harness/spec.md (product), .harness/prd.md (stories), clip.config.ts (typed config), wallet.identity.json (identity), docs/signing.md (stores and signing). ## Commands - pnpm install && pnpm harness && pnpm check-types && pnpm build (all must pass) -- pnpm extension:build | extension:dev | extension:zip | extension:build:fixtures + +- pnpm dev:extension | extension:build | extension:zip | extension:build:fixtures + + +- pnpm dev:desktop | desktop:build | desktop:start | desktop:dist | desktop:dist:mac | desktop:dist:win | desktop:dist:linux + + +- pnpm --filter mobile start | ios | android ; pnpm mobile:prebuild | mobile:export + + - pnpm next:dev | next:build | next:serve | lint -- pnpm wallet:identity [--name --rdns --homepage --description --accent --icon --walletconnect-project-id --new-key] -- pnpm wallet:listings | wallet:mainnet-check | verify:provenance + +- pnpm wallet:identity [--name --rdns --id --homepage --description --accent --logo --walletconnect-project-id --new-key] +- pnpm wallet:brand [--logo] | wallet:listings | wallet:mainnet-check | verify:provenance ## Rules (enforced by pnpm harness, tools/harness/check.mjs) - No key material in this project: phrases, keys and signing live in @clip-wallet/vault inside the kit. - The wallet's identity is its own: never org.coldai.* or "Clip Wallet". -- wxt.config.ts builds through clipWallet(); the phishing lists can't be switched off; never define __CLIP_SECURITY__. -- Mainnet only when every box in packages/extension/MAINNET.md is ticked by the owner (never by an agent). -- .env, .env.local, .keys/ and *.pem are never tracked; secrets are never logged. +- Every platform builds through its kit (clipWallet, clipDesktop, expoConfig + withClipWallet); the phishing lists can't be switched off; never define __CLIP_SECURITY__. +- Mainnet only when every box in MAINNET.md is ticked by the owner (never by an agent). +- .env, .env.local, .keys/ and *.pem are never tracked; secrets are never logged; certificates and store credentials only in CI secrets. - @clip-wallet/* and create-clip-wallet are pinned to one exact version. -## Wallet: packages/extension -- wallet.identity.json: name, description, rdns (EIP-6963), homepage, icon, extension.key (public key; fixes the Chrome extension id). -- clip.config.ts (@clip-wallet/config defineConfig): theme {accent, accentText, font, radius}; networks ("evm:*", "evm:", hedera, solana, bitcoin, sui, aptos, cardano, substrate, starknet, ton, near, stellar, tezos, algorand); route {mode, filters, settleOnHedera}; hardware [ledger, keystone]; passkeys {enabled, rpOrigin}; services {backupUrl, mediaProxyUrl, clipHandles}; compatibilityMode; mainnet (false | {enabled, acknowledged: MAINNET_ACKNOWLEDGEMENT}). -- wxt.config.ts: clipWallet({ config }) from @clip-wallet/extension-kit/wxt: manifest identity, 1Mask identity, security floor, mainnet checklist. +## The wallet (project root) +- wallet.identity.json: name, description, rdns (EIP-6963), homepage, icon, appId (desktop/phone id; default rdns), extension.key (public key; fixes the Chrome extension id). +- clip.config.ts (@clip-wallet/config defineConfig): theme {accent, accentText, font, radius}; networks ("evm:*", "evm:", hedera, solana, bitcoin, sui, aptos, cardano, substrate, starknet, ton, near, stellar, tezos, algorand); languages; scheme; desktop {appId}; mobile {bundleId, androidPackage}; route {mode, filters, settleOnHedera}; hardware [ledger, keystone]; passkeys {enabled, rpOrigin}; services {backupUrl, mediaProxyUrl, linkRelayUrl, clipHandles}; fees/usage (reserved for Clip Cloud, off); compatibilityMode; mainnet (false | {enabled, acknowledged: MAINNET_ACKNOWLEDGEMENT}). +- icon.svg | icon.png: the logo; pnpm wallet:brand renders every platform's icons from it. +- .env: CLIP_WALLETCONNECT_PROJECT_ID, CLIP_0X_API_KEY, CLIP_JUPITER_API_KEY, CLIP_COINGECKO_DEMO_KEY, CLIP_MOONPAY_*, CLIP_BANXA_PARTNER, CLIP_C14_*, CLIP_BLOCKAID_API_KEY, CLIP_EXTENSION_IDS, CLIP_ASSOCIATED_DOMAIN. + + +## Extension: packages/extension +- wxt.config.ts: clipWallet({ config, configDir: "../.." }) from @clip-wallet/extension-kit/wxt: manifest identity, 1Mask identity, security floor, mainnet checklist. - src/entrypoints: background (startBackground), content (startContentBridge), inpage.content (installInpage), popup/tab (mountWallet), approval (mountApprovalWindow), plugin-host (startPluginHost), plugin-sandbox. -- .env: CLIP_WALLETCONNECT_PROJECT_ID, CLIP_0X_API_KEY, CLIP_JUPITER_API_KEY, CLIP_COINGECKO_DEMO_KEY, CLIP_MOONPAY_*, CLIP_BANXA_PARTNER, CLIP_C14_*, CLIP_BLOCKAID_API_KEY. +- public/icon/{16,32,48,128}.png: rendered from the logo. + + + +## Desktop: packages/desktop (Electron, electron-vite, electron-builder) +- electron.vite.config.ts: clipDesktop({ config, configDir: "../.." }) from @clip-wallet/desktop-kit/electron-vite. +- electron-builder.config.cjs: electronBuilderConfig({ configFile }) from @clip-wallet/desktop-kit/builder: app id .desktop, deep-link scheme, dmg/zip, NSIS/zip, AppImage/deb/tar.gz. +- src/main/index.ts, src/preload/{wallet,dapp}.ts, src/renderer/{wallet,approval,browser}/main.tsx: one-line entrypoints. +- build/icon.icns, icon.ico, icons/*.png, src/renderer/public/tray/*.png: rendered from the logo. + + + +## Phone: packages/mobile (Expo, React Native) +- app.config.ts: expoConfig({ configFile }) from @clip-wallet/mobile-kit/expo: name, slug, scheme, bundle id, package, icons, splash, permissions. +- metro.config.js: withClipWallet() from @clip-wallet/mobile-kit/metro: the resolved config as virtual:clip-wallet/config. +- index.ts: polyfills, then registerClipWallet({ icon }). +- assets/{icon,adaptive-icon,adaptive-monochrome,splash-icon}.png: rendered from the logo. eas.json: optional EAS profiles. + + ## Dapp: packages/nextjs (Scaffold-HBAR, Next.js app router, wagmi + RainbowKit) - app/page.tsx: finds the wallet by EIP-6963 rdns, connect, sign (EIP-191, verified), send 0.1 HBAR, link to /debug. - app/clip-connect: Clip Connect (@clip-wallet/connect) demo: connect (prefers this wallet, falls back to any other), CAIP-10 accounts, EIP-5792 capabilities, balances by asset, pay() with ERC-7682 auxiliary funds or a plain transfer. @@ -33,14 +66,16 @@ Read first: AGENTS.md (rules + recipes), .harness/spec.md (product), .harness/pr - utils/wallet.ts: WALLET_NAME, WALLET_RDNS from wallet.identity.json; findWallet() (EIP-6963). - scaffold.config.ts: targetNetworks [hederaTestnet]. + ## Kit packages (npm, provenance, one version) -- @clip-wallet/extension-kit: the extension (background, pages, 1Mask wiring, WXT config). -- @clip-wallet/config: clip.config.ts schema, mainnetProblems, walletKey. -- Underneath: core, vault, 1mask, engine, ui, i18n, route, security, features, social, plugins, hardware, names, chains-* (14 families). -- create-clip-wallet: npx create-clip-wallet ; identity | listings | mainnet-check. +- @clip-wallet/extension-kit, @clip-wallet/desktop-kit, @clip-wallet/mobile-kit: the apps as libraries. +- @clip-wallet/config: clip.config.ts schema, platformIds, mainnetProblems, walletKey; /node: loadClipConfigSync. +- Underneath: core, vault, 1mask, engine, ui, i18n, route, security, features, social, plugins, hardware, names, link, chains-* (14 families). +- create-clip-wallet: npx create-clip-wallet [--platforms --scaffold-hbar …]; identity | brand | listings | mainnet-check. ## Optional -- packages/extension/MAINNET.md: what mainnet takes +- MAINNET.md: what mainnet takes +- docs/signing.md: code signing and store accounts - docs/listings/README.md: listing drafts for this identity - .harness/README.md: the hedera-harness recipe and validators - https://github.com/ColdAI-org/clip-wallet: the kit (AGENTS.md there for kit changes) diff --git a/templates/scaffold-hbar-clip-wallet/package.json b/templates/scaffold-hbar-clip-wallet/package.json index 8ba407e..cd85516 100644 --- a/templates/scaffold-hbar-clip-wallet/package.json +++ b/templates/scaffold-hbar-clip-wallet/package.json @@ -2,7 +2,7 @@ "name": "clip-wallet-project", "version": "0.1.0", "private": true, - "description": "Your own non-custodial wallet on the Clip Wallet kit, with a Scaffold-HBAR dapp that connects to it on Hedera testnet", + "description": "Your own non-custodial wallet on the Clip Wallet kit: a browser extension, a desktop app and a phone app from one clip.config.ts, with a Scaffold-HBAR dapp that connects to it on Hedera testnet", "license": "Apache-2.0", "homepage": "https://github.com/ColdAI-org/clip-wallet/tree/main/templates/scaffold-hbar-clip-wallet#readme", "bugs": { @@ -14,8 +14,18 @@ "directory": "templates/scaffold-hbar-clip-wallet" }, "scripts": { - "build": "pnpm extension:build && pnpm next:build", - "check-types": "pnpm extension:check-types && pnpm next:check-types", + "build": "pnpm extension:build && pnpm desktop:build && pnpm next:build", + "check-types": "pnpm extension:check-types && pnpm desktop:check-types && pnpm mobile:check-types && pnpm next:check-types", + "dev:desktop": "pnpm --filter @sh/desktop dev", + "dev:extension": "pnpm --filter @sh/extension dev", + "dev:mobile": "pnpm --filter @sh/mobile start", + "desktop:build": "pnpm --filter @sh/desktop build", + "desktop:check-types": "pnpm --filter @sh/desktop check-types", + "desktop:dist": "pnpm --filter @sh/desktop dist", + "desktop:dist:linux": "pnpm --filter @sh/desktop dist:linux", + "desktop:dist:mac": "pnpm --filter @sh/desktop dist:mac", + "desktop:dist:win": "pnpm --filter @sh/desktop dist:win", + "desktop:start": "pnpm --filter @sh/desktop start", "extension:build": "pnpm --filter @sh/extension build", "extension:build:fixtures": "pnpm --filter @sh/extension build:fixtures", "extension:check-types": "pnpm --filter @sh/extension check-types", @@ -24,6 +34,10 @@ "format": "pnpm next:format", "harness": "node tools/harness/check.mjs", "lint": "pnpm next:lint", + "mobile:check-types": "pnpm --filter @sh/mobile check-types", + "mobile:export": "pnpm --filter @sh/mobile export", + "mobile:prebuild": "pnpm --filter @sh/mobile prebuild", + "mobile:start": "pnpm --filter @sh/mobile start", "next:build": "pnpm --filter @sh/nextjs build", "next:check-types": "pnpm --filter @sh/nextjs check-types", "next:dev": "pnpm --filter @sh/nextjs dev", @@ -31,15 +45,17 @@ "next:lint": "pnpm --filter @sh/nextjs lint", "next:serve": "pnpm --filter @sh/nextjs serve", "verify:provenance": "node tools/verify-provenance.mjs", + "wallet:brand": "create-clip-wallet brand", "wallet:identity": "create-clip-wallet identity", "wallet:listings": "create-clip-wallet listings", "wallet:mainnet-check": "create-clip-wallet mainnet-check" }, "devDependencies": { + "@clip-wallet/config": "0.1.0", "create-clip-wallet": "0.1.0" }, "engines": { - "node": ">=22", + "node": ">=22.18", "pnpm": ">=10" } } diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/.gitignore b/templates/scaffold-hbar-clip-wallet/packages/desktop/.gitignore new file mode 100644 index 0000000..dcfe682 --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/packages/desktop/.gitignore @@ -0,0 +1,2 @@ +out/ +release/ diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/README.md b/templates/scaffold-hbar-clip-wallet/packages/desktop/README.md new file mode 100644 index 0000000..734bbf3 --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/packages/desktop/README.md @@ -0,0 +1,26 @@ +# The desktop app + +Your wallet for macOS, Windows and Linux: an [Electron](https://www.electronjs.org) app built with +[electron-vite](https://electron-vite.org) on [`@clip-wallet/desktop-kit`](https://www.npmjs.com/package/@clip-wallet/desktop-kit). +The kit is the app (vault and engine in the main process, the same screens as the extension, approvals, a built-in dapp +browser with 1Mask, Touch ID, Ledger, linked devices); this folder holds one-line entrypoints and the icons. Name, ids, +theme, networks, languages, deep links and services come from `../../clip.config.ts`. + +| File | What it is | +| --- | --- | +| `electron.vite.config.ts` | One line: `clipDesktop({ config })`. | +| `electron-builder.config.cjs` | One line: `electronBuilderConfig({ configFile })`: installers for every OS. | +| `src/main`, `src/preload`, `src/renderer/*/main.tsx` | One-line entrypoints into the kit. | +| `src/renderer/*/index.html` | The pages; `%CLIP_WALLET_NAME%` and `%CLIP_WALLET_CSP%` are filled in at build time. | +| `build/icon.icns`, `icon.ico`, `icons/`, `src/renderer/public/tray/` | Icons rendered from the logo (`pnpm wallet:brand`). | +| `build/entitlements.mac*.plist` | Hardened-runtime entitlements for a Developer ID build. | + +```sh +pnpm dev # run it with live reload (pnpm dev:desktop from the project root) +pnpm build && pnpm start # build to out/ and run the built app +pnpm dist # installers for this computer's OS in release/ (unsigned without certificates) +pnpm dist:mac # dmg + zip · dist:win: NSIS + zip · dist:linux: AppImage + deb + tar.gz +``` + +Windows installers build on Windows (or CI), Linux AppImage/deb on Linux; any OS can build the zip / tar.gz archives. +Signing and notarization: `../../docs/signing.md`. diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/build/entitlements.mac.inherit.plist b/templates/scaffold-hbar-clip-wallet/packages/desktop/build/entitlements.mac.inherit.plist new file mode 100644 index 0000000..d7e53ea --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/packages/desktop/build/entitlements.mac.inherit.plist @@ -0,0 +1,11 @@ + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.device.camera + + + diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/build/entitlements.mac.plist b/templates/scaffold-hbar-clip-wallet/packages/desktop/build/entitlements.mac.plist new file mode 100644 index 0000000..8fca686 --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/packages/desktop/build/entitlements.mac.plist @@ -0,0 +1,16 @@ + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.device.camera + + + diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/build/icon.icns b/templates/scaffold-hbar-clip-wallet/packages/desktop/build/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..c6382b1d73dba9a8a102c7db5c142575341eb0c5 GIT binary patch literal 69578 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 UIQ&^T5(4kWHDlf4s}6tuA814k{{R30 literal 0 HcmV?d00001 diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/build/icon.ico b/templates/scaffold-hbar-clip-wallet/packages/desktop/build/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..83ff399c0611d51e97a51945368dedcbf1a49da6 GIT binary patch literal 9259 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 literal 0 HcmV?d00001 diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/build/icon.png b/templates/scaffold-hbar-clip-wallet/packages/desktop/build/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2265327e056d41a5582bc96f9bd371fa92754bca GIT binary patch literal 13115 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 De;^(h literal 0 HcmV?d00001 diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/build/icons/1024x1024.png b/templates/scaffold-hbar-clip-wallet/packages/desktop/build/icons/1024x1024.png new file mode 100644 index 0000000000000000000000000000000000000000..2265327e056d41a5582bc96f9bd371fa92754bca GIT binary patch literal 13115 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 De;^(h literal 0 HcmV?d00001 diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/build/icons/128x128.png b/templates/scaffold-hbar-clip-wallet/packages/desktop/build/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..ca1b9574cc06d96b1d7ca5ecf186fb49a9db18f4 GIT binary patch literal 1991 zcmV;&2RQhNP)Qg;n69p_?y7oTcwi>!u8+5#bXC0{)k0X7D~}8- z3abh&g|0$hVW2QkkiKZ2EU;&q~c2lj7cNUfnQQsqoPX>jM0+q!LKW9 zP`Ta!V^y~`_$vyVRH}Etn620t{B0`JTWlMXKtJ~R%Mas8P&Zldan28GO|X=P09@?{ zxI0L*&!@t|L}L*PAB{X=8;h3b5uigQ1&_`=#|~}#arh2u7ZIRMA<_B}bidn(H+XdBu{0$9c?&u_`!emRuB2-=D^lK>X*@dx+i{{B?@CTKI-P6C+D z^^cFv_v5|``hdQW04Z>-d0XE4<4A@g=nMKp0xW=6U%XT3)-KO2Bl<=H%)x8dx8=?_ zyuqId`i4G|0GIsoLmvDj3Ci>&5M`P3^Lp%`ok{uUtz)_V)kJ>s;eGkW>v!eLSGQ#I z$$u7R_#2<$J6wZnanIRVe$zo&)&!yqt9{!GzNy+ZZv1+9KA2aZHdi$6MHwh-I<>zr zEW_@ga01xe>Kz?Th zEfPRCK@$L>Q&}Q;%JkTuHyvb&EOH%Q=9B=4pUbk~^CikA%fb*k3X%Ybpfxr3V!Ku3 zYTsmcV7f(wj$B3?J0k$Xs7;MLMeOr!*a=LHMbK4n1VAjkujW$MW^U*zC<0)6W0JH1 zySoeXZk}di6W;-JkU80Me^X1c4Z~{SS+T zoZ%YVc6s#0L|mk%=S~3R*qfMqvqbVF@zi5xQxNXKy*Ao_zL=PZp*!yhfP)s&3gYpK zTz}*-52H_M1-kQ|0496kbxW)NtZrwUZrXK&-GB=lNbUr{kt;>a|5(h~2s1|+HWr2^ z>j3D^djjCxowQ=Z^otd}EkRi}Ov7mfy7Qg@ChlLv=x41L+d5l1DnH zzA(Zw0`QeFkrM#mOQRwu0KnJAMNR;KFOH0y0JA3nUmY7U0cL9gzC1c&0?c*?e0_Yx z1V|c-Y*-9k3))K&?1Z)vGgX%CniZ7s0($v zjNZ)&6A)nFd-_#!dO%7Q^6EwweD%phO_YhUeSFN%!2ks42h^eeq^2Xs9tSO8KaL|; zaPAJ~?BwZX_#2<$J6wZnaS!fwk)mB7`XE3z;0_Y=(FFloR7A9BfdH#iM6_510V)c6 zR7P~bm{$Zu{n4XBqC-#2ME%pGLZU-6Nr0-tm`aHT80)G?iu#~U#YBU)cv#ejHH8Bz zCwO3tYvLkNUv#LT;L#D6iTZL`VMrwf3ykfum?!Fj1{D=78e$Pq7p_rR!C_4-D(d1T zg$*h#7+@@yL>f^y)D+Nkh=p9R{N#8WD=orIQ9c|pzc?SdEGmu}Z|!wGLZarxn}d Z^*6x9T~Dume?9;J002ovPDHLkV1m3}oz(yU literal 0 HcmV?d00001 diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/build/icons/16x16.png b/templates/scaffold-hbar-clip-wallet/packages/desktop/build/icons/16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..b54180d609dc95c686850375d0065b019fdcfeee GIT binary patch literal 232 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

baRo?ewoN8~ZSNcC9ADiP)P<8p_ zlw$)+Rp1a8TIwcb_s<*5^c2v)jh|F=q3ioLjt1d707GvpiX`>tx5XzAO~kj(Ov#~x z^-*`H-}U!GrBzr~V6|bBJTHqA78hF2lhUi9Y}~SWkUKVZlKbsq&1rT%IPd!M4)z~H zT*3>X9^cSfRc#}#>!67CAjVXLVJXfqzm?PM!WHn22^y}11)RNSX3AFF_V4jMW9K@1 zcM&qY^1D7gKOoE!b$8#bG@tI`q8U&#rkHNLyn8IuPc?w;8P9d%Su%u8#DR=Vst3Y? z^^2I~K&~r<^QN6EqUyUDQJLav6*Zf)6w@|MK3%b@D}U$gF)!fJ+z5)yTQU^<;&IZQ zh-sw}d%8WdiI`oemaE-)CEQhlm3eoHkx{PyvQ;F30pxU##&b53JL09@=O@$+oIhfN zPPfKTxmWq;U|+>B3Y#L-eJ$$GSE<7HUsD~YtLs#aW_qVue^6ov3IW5UcJ+s-d>(<^ zlH-7nM@?uzTjDIolQ=Xxl)ruzZfH;5_3I`Rt+o@sv~nStuxti872T%AY*={`yJl00 zEB*e~v%w5m3ePL>PYvjK`;_3ZHBAWNpA4N444~cV_6_)G`@HJp_Je`(%asHU3OA08 zn4xi9$^t;ogS^UnY)yLTe3d}$cIqn#bok?F4O)yQs+NDjECOTk)0x|HN(Ej_oQjq! zC8`Fiew^=6-h;*wCdoFnTnJo>^$TN5(z-EvygS**m9C!U(2=~>@wtk~@ot0iovy6M zu2)ryZr_vtLcNmi<-#aHtg9UGGu_bwb+nBIL4^g@Dd}-vepkG88m^5HpZ!tnKl3ve zuS|Jz8n~}hT9CfKnj=gR_QiCWv(fd;QqN_p`K*ZvTuYCd*3Ee>2ro}6tilwZ6Tl-v z{MqogCD9HZAa-X{-sdH*@Dg$od9=uMPwyJTin*7*!c|?Fqb7}`MUy!;*n~Y*V=Z~f zl=I?P_?R+*)lZ7TcvX=wr9hUcD^egUAV>mp=x-?gF0C=DxA!m5a5jH$cXT+7eI6l)l^ExDmSUGn12Y*EIn7s7v~m7#{YL5sZWYW zOUo5}8P|5#GQ3TuFNu3*Yu2it>WO`G$DGk@H6>xr?-r8X-j3I*|l3l_%b zhN#OO0qJ(}fetx%8-E8K)Dlgd0h^_WJ8V%iKAESu0CWHRYA*NgnM~YuU@p~s7o?&P zcv;v74S;kAhgr3e-0^AXrt0c zXun9aZZ+n3+d6kCgFNvf>$?*Ti86&@)CE%Q=zrd%bd>@qPwPw;?v6*XJ%^iU+3e!J zM%*RiEHUZm1p;3u*hSLcuTsX(Ie;W~F*Or{MxnbbB&^SXDJOo~k>hAXEXNER3sMdK z4uXmXHsrsO(E=Qv7Y?XSvz`M_I0&0Q9Ha^{CIix08(Y%)5Do)PlO3{)-L6uwPOP$m zrX%++FbJDwDq`WZZOZh4tT7ujdZV0UcuC%XP-{uU^J$Ak>s6QGhBjt4 z@0pB>qgB9LFR!{%nvGsq`5XNs`cT2vGA&8(taJnzVc33uv zi;{^050;(TnYS~u$*xjNnvKBI7--Bjjv90$95bKr?J1jRg|D%_VbDAvhHndgsv!6o zr#})|+P4)3=b7+SRu>Y&KxNG#MPRO6y1o6lptskEK4*Jc|0I+olmcVSK?fU`)1fXk z9`+;JggRP@zy`LL`Yk2UdI)Ixu_IT(8mzsVfGzm<*MTOZm|l9D6c}U9M&c?0NF^84 zy4MYJ?V*DWm$GuGqK4x&hvR2|Rk|bCa1!uQ)R?&h(9v9qc5cfA7x;1}fMdmLZt9;p zOL`y2DKIwmyeHtxnE+2Cn#>FyuK)EInjv&UM(eDVXCnSaTNmx<3Nq zB@k~y@nb0dArk)-*WzbXi{Eh-7ZUL+nfRqt{8}#iFcE*6)#7m~`8@vxi$k}2p?9OA P00000NkvXXu0mjfptihh literal 0 HcmV?d00001 diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/build/icons/48x48.png b/templates/scaffold-hbar-clip-wallet/packages/desktop/build/icons/48x48.png new file mode 100644 index 0000000000000000000000000000000000000000..3a03115ca57d5fcf80a21b8fa44ddccef6882f41 GIT binary patch literal 624 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 z zlEUSYO;kL6b?2qe4;b48ym&la$LDp=y7T6l_>)~afUa{@thx16V=U00R#xbcawh|} z^Ima>a6_?N84b|23q%iRN_7^gKNSG?%=Gk#8b@wNqT6aaU`0OCnzg9P#whNMG~ms7 zNGh^weQ{vC!}y;?((GM5v#MPiN7Ciguw~Zo+M6B&P$0CgQ;4IX;I)Rp?9cs(N^Y>n zip`)kY*Mr6=QSDNJ*Y+`BRg<_cyCTYnx5Jrf|*3liF{h0i5wx5|%t4>#0ilh~%hA-~eiUVjRF z2fte=?>7g$ACY2z(WUYN<0J=}ikCZ=Pn#`4uUA{Pi3QzHk8#z?X;PYd9qaNMfC-Pa zNp1ym{zAI#Q76TUmtil>U-+L3adLMB+i#SxV!|%JZ2Kp-IiyvYqdg&6Gxfr)c4^{4 P0Dq45yX;DAz0dz27Q+dp literal 0 HcmV?d00001 diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/build/icons/64x64.png b/templates/scaffold-hbar-clip-wallet/packages/desktop/build/icons/64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..3febf35ba7223887f821ee33340a18cfc9650fc8 GIT binary patch literal 818 zcmV-21I_%2P)MSu1-*zTgGarIH$Ca0c=O`PLlG(>g7^cklH$QC;*V2KmR5|!B>s0AK}3NRbr`T4SvaJdyax1Dlsi( z3I1RXgCFXF6@;YQx92$c(VkEpm+;mD2!1ptR7L@)1r+>HYop_QBR~Ke1D=o&1_@Lb zql7hu3czxdP{&XKs7DEZ_S(>ydT}Bo7kRN0M4DBRE! z{l2_-wkxYEt$}jQJ)Y$OVG=!|Gyqw;ecz;nwKscm^TsbZd%|vuXOy9Azn|@M_^$wv zlw9)lb62ijY38;~9qPKIY+{Vc40UPQ9)k1eVY?Kep)NM!IDK>|Gei~!fU!9(`1|*M zPb^boD-8sEnJNIVvow;!TbuNFjm0C2>{?aACl z5?-FfXd4~ysR;170br(P6NWF}GdS9?3BGv;fa$7D7{jyJxP5C|zI^IRw;KKwIMk-@F6B z>diQT&%5f<)m=gvmmS&~C-~+a02?7;Qsb)2_wEwPxa`oDjesct`Uz+f)L~GGkWyit z4C!=O7esVPOczCUSzLdN=&v#TIjX; +out.clearLine ??= () => true; +out.cursorTo ??= () => true; +out.moveCursor ??= () => true; + +export default defineConfig(clipDesktop({ config: clipConfig, root: import.meta.dirname, configDir: "../.." }) as never); diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/package.json b/templates/scaffold-hbar-clip-wallet/packages/desktop/package.json new file mode 100644 index 0000000..0cd38ed --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/packages/desktop/package.json @@ -0,0 +1,42 @@ +{ + "name": "@sh/desktop", + "version": "0.1.0", + "private": true, + "license": "Apache-2.0", + "homepage": "https://github.com/ColdAI-org/clip-wallet/tree/main/templates/scaffold-hbar-clip-wallet/packages/desktop#readme", + "bugs": { + "url": "https://github.com/ColdAI-org/clip-wallet/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ColdAI-org/clip-wallet.git", + "directory": "templates/scaffold-hbar-clip-wallet/packages/desktop" + }, + "type": "module", + "main": "out/main/index.mjs", + "scripts": { + "dev": "electron-vite dev", + "build": "electron-vite build", + "start": "electron .", + "check-types": "tsc --noEmit", + "dist": "electron-vite build && electron-builder --config electron-builder.config.cjs --publish never", + "dist:dir": "electron-vite build && electron-builder --dir --config electron-builder.config.cjs --publish never", + "dist:mac": "electron-vite build && electron-builder --mac --config electron-builder.config.cjs --publish never", + "dist:win": "electron-vite build && electron-builder --win --config electron-builder.config.cjs --publish never", + "dist:linux": "electron-vite build && electron-builder --linux --config electron-builder.config.cjs --publish never" + }, + "devDependencies": { + "@clip-wallet/config": "0.1.0", + "@clip-wallet/desktop-kit": "0.1.0", + "@types/node": "^22.0.0", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "electron": "44.5.1", + "electron-builder": "^26.15.3", + "electron-vite": "^5.0.0", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "typescript": "^5.9.3", + "vite": "^7.1.0" + } +} diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/src/main/index.ts b/templates/scaffold-hbar-clip-wallet/packages/desktop/src/main/index.ts new file mode 100644 index 0000000..188981f --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/packages/desktop/src/main/index.ts @@ -0,0 +1,2 @@ +// The desktop app is @clip-wallet/desktop-kit; this project is the brand (../../clip.config.ts and the icons). +import "@clip-wallet/desktop-kit/main"; diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/src/preload/dapp.ts b/templates/scaffold-hbar-clip-wallet/packages/desktop/src/preload/dapp.ts new file mode 100644 index 0000000..5cbca18 --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/packages/desktop/src/preload/dapp.ts @@ -0,0 +1 @@ +import "@clip-wallet/desktop-kit/preload/dapp"; diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/src/preload/wallet.ts b/templates/scaffold-hbar-clip-wallet/packages/desktop/src/preload/wallet.ts new file mode 100644 index 0000000..4bde001 --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/packages/desktop/src/preload/wallet.ts @@ -0,0 +1 @@ +import "@clip-wallet/desktop-kit/preload/wallet"; diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/approval/index.html b/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/approval/index.html new file mode 100644 index 0000000..4c64780 --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/approval/index.html @@ -0,0 +1,13 @@ + + + + + + + %CLIP_WALLET_NAME% + + +

+ + + diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/approval/main.tsx b/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/approval/main.tsx new file mode 100644 index 0000000..9eb3f6b --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/approval/main.tsx @@ -0,0 +1,3 @@ +import { mountApproval } from "@clip-wallet/desktop-kit/renderer"; + +void mountApproval(); diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/browser/index.html b/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/browser/index.html new file mode 100644 index 0000000..89a67ba --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/browser/index.html @@ -0,0 +1,13 @@ + + + + + + + Browser + + +
+ + + diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/browser/main.tsx b/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/browser/main.tsx new file mode 100644 index 0000000..b2aa8e2 --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/browser/main.tsx @@ -0,0 +1,3 @@ +import { mountBrowserChrome } from "@clip-wallet/desktop-kit/renderer"; + +mountBrowserChrome(); diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/public/tray/tray.png b/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/public/tray/tray.png new file mode 100644 index 0000000000000000000000000000000000000000..b54180d609dc95c686850375d0065b019fdcfeee GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`>pfi@Ln>}1ConM8Z(knhP{Z5t zpYdZo(+~NOToL9;&;F-8(qLPW`9!LqmvQ#y+tT;{?XUXz{QrFW|BZ$nyh4Yc$+DFk z{JPryKYQc(`Tzgfm;JDoXuhGqFv&Umd_$fw#~juY(5!#s>98?RhA$I!Lcp-^FxbgziIO4nqg f)R~=^-taI)Z`kmdMYs4k(CG}Gu6{1-oD!MGA&8(taJnzVc33uv zi;{^050;(TnYS~u$*xjNnvKBI7--Bjjv90$95bKr?J1jRg|D%_VbDAvhHndgsv!6o zr#})|+P4)3=b7+SRu>Y&KxNG#MPRO6y1o6lptskEK4*Jc|0I+olmcVSK?fU`)1fXk z9`+;JggRP@zy`LL`Yk2UdI)Ixu_IT(8mzsVfGzm<*MTOZm|l9D6c}U9M&c?0NF^84 zy4MYJ?V*DWm$GuGqK4x&hvR2|Rk|bCa1!uQ)R?&h(9v9qc5cfA7x;1}fMdmLZt9;p zOL`y2DKIwmyeHtxnE+2Cn#>FyuK)EInjv&UM(eDVXCnSaTNmx<3Nq zB@k~y@nb0dArk)-*WzbXi{Eh-7ZUL+nfRqt{8}#iFcE*6)#7m~`8@vxi$k}2p?9OA P00000NkvXXu0mjfptihh literal 0 HcmV?d00001 diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/public/tray/trayTemplate.png b/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/public/tray/trayTemplate.png new file mode 100644 index 0000000000000000000000000000000000000000..f780b8a160db50028ebbbb578f7677d9a8f7a755 GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`DV{ElAr-fh6C}1N2z2l>ek|9p zV-Z#6`oi$BUZIZtNcMssvLS2QC1yKvZD%&LpJZVl^2T0qx=hn8w##ccjv6@MbG-V1 z<^Dpyw#(X!Cb^mV&&^`uFS3j3^P6wZemTu(v7Lvw5t=biY + + + + + + %CLIP_WALLET_NAME% + + +
+ + + diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/wallet/main.tsx b/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/wallet/main.tsx new file mode 100644 index 0000000..fd4ecc3 --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/packages/desktop/src/renderer/wallet/main.tsx @@ -0,0 +1,3 @@ +import { mountWallet } from "@clip-wallet/desktop-kit/renderer"; + +void mountWallet(); diff --git a/templates/scaffold-hbar-clip-wallet/packages/desktop/tsconfig.json b/templates/scaffold-hbar-clip-wallet/packages/desktop/tsconfig.json new file mode 100644 index 0000000..da34f3f --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/packages/desktop/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "types": ["node"] + }, + "include": ["src", "electron.vite.config.ts", "../../clip.config.ts"] +} diff --git a/templates/scaffold-hbar-clip-wallet/packages/extension/README.md b/templates/scaffold-hbar-clip-wallet/packages/extension/README.md index b3ec7ee..c275e0a 100644 --- a/templates/scaffold-hbar-clip-wallet/packages/extension/README.md +++ b/templates/scaffold-hbar-clip-wallet/packages/extension/README.md @@ -1,19 +1,19 @@ # The wallet extension -This package is your wallet: a Manifest V3 browser extension built with [WXT](https://wxt.dev) on +Your wallet as a Manifest V3 browser extension, built with [WXT](https://wxt.dev) on [`@clip-wallet/extension-kit`](https://www.npmjs.com/package/@clip-wallet/extension-kit). The kit is the wallet -(background, vault, approvals, security checks, 1Mask for 14 network families, the pages); this folder is the brand. +(background, vault, approvals, security checks, 1Mask for 14 network families, the pages); this folder holds one-line +entrypoints and the toolbar icons. Name, theme, networks, languages and services come from `../../clip.config.ts`, the +config every platform of this wallet shares. | File | What it is | | --- | --- | -| `wallet.identity.json` | Name, description, rdns, homepage, icon, extension public key. Written by `pnpm wallet:identity`. | -| `clip.config.ts` | Theme, networks, routing, hardware wallets, passkeys, services, mainnet. Typed by `@clip-wallet/config`. | -| `icon.svg`, `public/icon/*.png` | Your icon (EIP-6963 and pages use the SVG; the manifest needs the PNGs). | -| `wxt.config.ts` | One line: `clipWallet({ config })`. | +| `wxt.config.ts` | One line: `clipWallet({ config, configDir: "../.." })`. | | `src/entrypoints/*` | One-line entrypoints that start the kit's background, content scripts and pages. | -| `.env` | `CLIP_*` build-time values (WalletConnect project id, partner keys). Never committed. | -| `.keys/extension.pem` | The extension's private key. Never committed; back it up. | -| `MAINNET.md` | The checklist that stands between this wallet and real money. | +| `public/icon/*.png` | The manifest's icons, rendered from the logo (`pnpm wallet:brand`). | +| `../../wallet.identity.json` | Name, description, rdns, homepage, icon, extension public key. Written by `pnpm wallet:identity`. | +| `../../.env` | `CLIP_*` build-time values (WalletConnect project id, partner keys). Never committed. | +| `../../.keys/extension.pem` | The extension's private key. Never committed; back it up. | ```sh pnpm build # .output/chrome-mv3: load it unpacked in chrome://extensions diff --git a/templates/scaffold-hbar-clip-wallet/packages/extension/package.json b/templates/scaffold-hbar-clip-wallet/packages/extension/package.json index 699b636..104789c 100644 --- a/templates/scaffold-hbar-clip-wallet/packages/extension/package.json +++ b/templates/scaffold-hbar-clip-wallet/packages/extension/package.json @@ -13,9 +13,6 @@ "directory": "templates/scaffold-hbar-clip-wallet/packages/extension" }, "type": "module", - "exports": { - "./wallet.identity.json": "./wallet.identity.json" - }, "scripts": { "dev": "wxt", "dev:firefox": "wxt -b firefox --mv3", diff --git a/templates/scaffold-hbar-clip-wallet/packages/extension/public/icon/128.png b/templates/scaffold-hbar-clip-wallet/packages/extension/public/icon/128.png index 1f033303931d1357601342bc23ec456abc997cf5..ca1b9574cc06d96b1d7ca5ecf186fb49a9db18f4 100644 GIT binary patch delta 1978 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 JPDHLkV1goD*r@;j diff --git a/templates/scaffold-hbar-clip-wallet/packages/extension/public/icon/16.png b/templates/scaffold-hbar-clip-wallet/packages/extension/public/icon/16.png index 084ac06392b32d3ec17be0de0612e6463250abd4..b54180d609dc95c686850375d0065b019fdcfeee 100644 GIT binary patch delta 204 zcmV;-05ku}0q6mcB!90-L_t(|+GAh<0)MxsQ9yhRi2nice**X)RBaRklIH(p;wT2F zY2+GkjRBjp+Z*5hzk2!S|DQkqfiZ{;62s;l26Vyot3Uq-fzO}+{y%p3I~ar5AOMm> zSI>ZMGYFhL_2Yk3;B&Y*hz$}00d$w37yxoA$Tpbfw73^w3`H{pBnMK1(*TfvK~9Eg z{`Ko07{d$!$$`|+&H#$NKye_F8;ukvB#M(VWtoZ6>6S69+JmXfuXO z7_dpSHN5?Q`Qp$2FaTnM#IUKwFktVluV77IKK})41Yr;x1VD0l4FK5!voR{@Ib0mX zhPepcB`5~KY=db|jeP;eFhgLj!f61^c9>=ufEfaF1?>!=*e44V2O`DMNNz%+I4M({ cnJCK60B$(A%U@_i%>V!Z07*qoM6N<$f(AcGUH||9 diff --git a/templates/scaffold-hbar-clip-wallet/packages/extension/public/icon/32.png b/templates/scaffold-hbar-clip-wallet/packages/extension/public/icon/32.png index 84b34baaecc919c09208394478a6455858e371de..bf0947f5d9dc1b03cc926a33f5ebaf862e0125bd 100644 GIT binary patch delta 395 zcmV;60d)TF0;L0xB!6s4L_t(|+SOOVYQjJe-S5Z;Ne=}P^b+W$pWw|?Z%RSvNe}ij zdhi2;UQ5AWh*~2dh@e7nx-Z0bST>1^l8FNkmYvy|w==WJu2M{zjlk0wXv{T^8gwHZ zGoSG7DVu18ud%&h&^#c9Zwr2^Aov-lKN4Bmw-pBGnebFr7k?7MKxNG#MPRO6y1o6l zptskEK4*Jc|0I+olmcVSK?fU`)1fXk9`+;JggRP@zy`LL`Yk2UdI)Ixu_IT(8mzsV zfGzm<*MTOZm|l9D6c}U9M&c?0NF^84y4MYJ?V*DWm$GuGqK4x&hvR2|Rk|bCa1!uQ z)R?&h(9v9qc7JZm1Q+;nCV*qbYi{bFI!k&V$0;y2^}HwG%b5UABbv+%9K-~xuPJX+- zf9)u95g3yv3{6%6Y3`?!+eGp({GR~v5QrzC_%;+jh+1(Ji(f{qxQ<)#AgTLFCjKZD mf0m1XOvJxtt@xaZea}C8_}fn$Yh@4s0000Vq zT!rFMP_TDFk%D*<@2#M1X_spK+3lEb*~+?`bhB=(oyCEHkW4c3C7GFQvO;KRd9GQM zT#?+6>`105vlHls4Mkx}Qn(ER%8DEw>m_g&m41e3olZ(UGV0I<8OYj7b|UK_Yo!M|K5~2`O$oB-gT7hyX9BRd+ogWb-0&C>*%xC>9YATm zN$01}WQ#Bs#($g&04Tb=7$zF^@jjyVlj}3eNrW+f0wOhlNi*N=xKyh;w6Xs9%@~wX zhc?JeT8Uo;fc>f>SK3rNbZB)cIl4dwvaSY4TAvVUqK(R7hc6@A+H9wuIrKqam1C!g zPR0Nb_EaLN&e$&}j_-&M{F!SkxJRd-EETN1YA?#)=Yj zS_2T>|42-_q=VVk5!!UgF`De4(;5IT;=jnQI$3NP9idIv4`YT2I{yLWW(%=*V)kK_ zeIjFjlu=-Rn9Id+n*9e&ET&2 delta 473 zcmV;~0Ve+N1o8urBYy$8Nkl6;PPVOr3iEVDF8{~yMl0%!cQMQR=&Nr`?xj^ZFq-1k{}${ z=R*`5lvU7&h<_Usj+1SufJNKE{u5hs7!#V0lbxaggtWGa@O2O&Yl;J)w8dG7qZq6u z3V`x`1f#@<2y;+@9UBW#08j#OwSRK_vML|Ic;;FGYl;D&Y8xt5R(yN&zSbXYq5hbQ zs-46DP}HXR5Vcu?v&OFipsW?6)qen{T}XWpt52iqKT>4sO9>OjoceNF{f5TGmeRzg z-o&wriGwW@M~g!_+*N expoConfig({ configFile: "../../clip.config.ts", root: __dirname }); diff --git a/templates/scaffold-hbar-clip-wallet/packages/mobile/assets/adaptive-icon.png b/templates/scaffold-hbar-clip-wallet/packages/mobile/assets/adaptive-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f11b7b859bc2ba81b78f7ee56dfd8af973371dc1 GIT binary patch literal 14598 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 DNfH)j literal 0 HcmV?d00001 diff --git a/templates/scaffold-hbar-clip-wallet/packages/mobile/assets/adaptive-monochrome.png b/templates/scaffold-hbar-clip-wallet/packages/mobile/assets/adaptive-monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..f11b7b859bc2ba81b78f7ee56dfd8af973371dc1 GIT binary patch literal 14598 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 DNfH)j literal 0 HcmV?d00001 diff --git a/templates/scaffold-hbar-clip-wallet/packages/mobile/assets/icon.png b/templates/scaffold-hbar-clip-wallet/packages/mobile/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..099be3f7c9f30df0167c6e633649e8cece6a7426 GIT binary patch literal 11620 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 literal 0 HcmV?d00001 diff --git a/templates/scaffold-hbar-clip-wallet/packages/mobile/assets/splash-icon.png b/templates/scaffold-hbar-clip-wallet/packages/mobile/assets/splash-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1f7084ad21ca4d117207cf6b2c97d17c29d0fb01 GIT binary patch literal 10394 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 r9?;~xRcF$Gq$*?mQ= 16.0.0", "appVersionSource": "local" }, + "build": { + "development": { "developmentClient": true, "distribution": "internal" }, + "preview": { "distribution": "internal", "android": { "buildType": "apk" } }, + "production": { "autoIncrement": true } + }, + "submit": { "production": {} } +} diff --git a/templates/scaffold-hbar-clip-wallet/packages/mobile/index.ts b/templates/scaffold-hbar-clip-wallet/packages/mobile/index.ts new file mode 100644 index 0000000..dd70569 --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/packages/mobile/index.ts @@ -0,0 +1,6 @@ +// The phone app is @clip-wallet/mobile-kit; this project is the brand (../../clip.config.ts and assets/). +// Polyfills first: crypto.getRandomValues, TextEncoder, URL, Buffer, randomUUID. +import "@clip-wallet/mobile-kit/polyfills"; +import { registerClipWallet } from "@clip-wallet/mobile-kit"; + +registerClipWallet({ icon: require("./assets/icon.png") }); diff --git a/templates/scaffold-hbar-clip-wallet/packages/mobile/metro.config.js b/templates/scaffold-hbar-clip-wallet/packages/mobile/metro.config.js new file mode 100644 index 0000000..e791900 --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/packages/mobile/metro.config.js @@ -0,0 +1,5 @@ +// Metro: Expo's defaults plus the kit's wiring (the resolved clip.config, Node shims, one React). +const { getDefaultConfig } = require("expo/metro-config"); +const { withClipWallet } = require("@clip-wallet/mobile-kit/metro"); + +module.exports = withClipWallet(getDefaultConfig(__dirname), { configFile: "../../clip.config.ts", root: __dirname }); diff --git a/templates/scaffold-hbar-clip-wallet/packages/mobile/package.json b/templates/scaffold-hbar-clip-wallet/packages/mobile/package.json new file mode 100644 index 0000000..d151f7a --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/packages/mobile/package.json @@ -0,0 +1,67 @@ +{ + "name": "@sh/mobile", + "version": "0.1.0", + "private": true, + "license": "Apache-2.0", + "homepage": "https://github.com/ColdAI-org/clip-wallet/tree/main/templates/scaffold-hbar-clip-wallet/packages/mobile#readme", + "bugs": { + "url": "https://github.com/ColdAI-org/clip-wallet/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ColdAI-org/clip-wallet.git", + "directory": "templates/scaffold-hbar-clip-wallet/packages/mobile" + }, + "main": "index.ts", + "scripts": { + "start": "expo start --dev-client", + "ios": "expo run:ios", + "android": "expo run:android", + "prebuild": "expo prebuild --clean --no-install", + "export": "expo export --platform ios --platform android --output-dir dist", + "export:ios": "expo export --platform ios --output-dir dist", + "export:android": "expo export --platform android --output-dir dist", + "config": "expo config --type public", + "check-types": "tsc --noEmit", + "eas:build": "eas build --profile preview --platform all" + }, + "dependencies": { + "@clip-wallet/config": "0.1.0", + "@clip-wallet/mobile-kit": "0.1.0", + "@ledgerhq/react-native-hw-transport-ble": "6.41.0", + "@react-native-async-storage/async-storage": "2.2.0", + "@react-native-community/netinfo": "12.0.1", + "@walletconnect/react-native-compat": "2.25.0", + "expo": "~57.0.26", + "expo-application": "~57.0.3", + "expo-background-task": "~57.0.21", + "expo-camera": "~57.0.6", + "expo-clipboard": "~57.0.2", + "expo-crypto": "~57.0.3", + "expo-dev-client": "~57.0.19", + "expo-device": "~57.0.2", + "expo-linking": "~57.0.11", + "expo-local-authentication": "~57.0.3", + "expo-localization": "~57.0.2", + "expo-notifications": "~57.0.21", + "expo-secure-store": "~57.0.4", + "expo-splash-screen": "~57.0.9", + "expo-status-bar": "~57.0.1", + "expo-task-manager": "~57.0.21", + "expo-web-browser": "~57.0.3", + "react": "19.2.3", + "react-native": "0.86.3", + "react-native-argon2": "4.0.0", + "react-native-ble-plx": "3.4.0", + "react-native-get-random-values": "~1.11.0", + "react-native-passkey": "3.6.2", + "react-native-safe-area-context": "~5.7.0", + "react-native-svg": "15.15.4", + "react-native-webview": "13.16.1" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "~19.2.0", + "typescript": "^5.9.3" + } +} diff --git a/templates/scaffold-hbar-clip-wallet/packages/mobile/tsconfig.json b/templates/scaffold-hbar-clip-wallet/packages/mobile/tsconfig.json new file mode 100644 index 0000000..375de84 --- /dev/null +++ b/templates/scaffold-hbar-clip-wallet/packages/mobile/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "strict": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["index.ts", "app.config.ts", "../../clip.config.ts"], + "exclude": ["node_modules", "dist", "ios", "android"] +} diff --git a/templates/scaffold-hbar-clip-wallet/packages/nextjs/package.json b/templates/scaffold-hbar-clip-wallet/packages/nextjs/package.json index 830ce7e..d77286b 100644 --- a/templates/scaffold-hbar-clip-wallet/packages/nextjs/package.json +++ b/templates/scaffold-hbar-clip-wallet/packages/nextjs/package.json @@ -30,7 +30,6 @@ "@scaffold-hbar-ui/components": "^2.0.0", "@scaffold-hbar-ui/debug-contracts": "^2.0.0", "@scaffold-hbar-ui/hooks": "^1.1.0", - "@sh/extension": "workspace:*", "@tanstack/react-query": "^5.59.15", "blo": "^1.2.0", "burner-connector": "0.0.20", diff --git a/templates/scaffold-hbar-clip-wallet/packages/nextjs/scaffold.config.ts b/templates/scaffold-hbar-clip-wallet/packages/nextjs/scaffold.config.ts index db80070..0f8736d 100644 --- a/templates/scaffold-hbar-clip-wallet/packages/nextjs/scaffold.config.ts +++ b/templates/scaffold-hbar-clip-wallet/packages/nextjs/scaffold.config.ts @@ -9,8 +9,8 @@ export type ScaffoldConfig = { }; /** - * Hedera testnet only, like the wallet (packages/extension/clip.config.ts). Add chains.hedera here only after the - * wallet itself has passed its mainnet checklist (packages/extension/MAINNET.md). + * Hedera testnet only, like the wallet (clip.config.ts at the project root). Add chains.hedera here only after the + * wallet itself has passed its mainnet checklist (MAINNET.md). */ const targetNetworks = [chains.hederaTestnet] as const satisfies readonly [chains.Chain, ...chains.Chain[]]; diff --git a/templates/scaffold-hbar-clip-wallet/packages/nextjs/utils/wallet.ts b/templates/scaffold-hbar-clip-wallet/packages/nextjs/utils/wallet.ts index ae25e5a..6049cb4 100644 --- a/templates/scaffold-hbar-clip-wallet/packages/nextjs/utils/wallet.ts +++ b/templates/scaffold-hbar-clip-wallet/packages/nextjs/utils/wallet.ts @@ -1,8 +1,9 @@ /** - * The wallet this dapp is built to show off: the identity in packages/extension/wallet.identity.json (written by - * `pnpm wallet:identity`), so the dapp and the extension always agree on the name and the EIP-6963 rdns. + * The wallet this dapp is built to show off: the identity in the project's wallet.identity.json (written by + * `pnpm wallet:identity`, read by clip.config.ts), so the dapp and the wallet always agree on the name and the + * EIP-6963 rdns. */ -import identity from "@sh/extension/wallet.identity.json"; +import identity from "../../../wallet.identity.json"; export const WALLET_NAME: string = identity.name; /** EIP-6963 reverse-DNS id the extension announces (also the wagmi connector id). */ diff --git a/templates/scaffold-hbar-clip-wallet/pnpm-workspace.yaml b/templates/scaffold-hbar-clip-wallet/pnpm-workspace.yaml index 89c02d9..121d57a 100644 --- a/templates/scaffold-hbar-clip-wallet/pnpm-workspace.yaml +++ b/templates/scaffold-hbar-clip-wallet/pnpm-workspace.yaml @@ -5,6 +5,10 @@ packages: # through an install script; the native add-ons below have pure-JS fallbacks, so they stay off. allowBuilds: esbuild: true + # The desktop app (packages/desktop): downloads the Electron binary for this computer. + electron: true + # electron-builder's Squirrel.Windows helper; not used (NSIS + zip targets only). + electron-winstaller: false '@parcel/watcher': false bufferutil: false keccak: false diff --git a/templates/scaffold-hbar-clip-wallet/template.json b/templates/scaffold-hbar-clip-wallet/template.json index 807501d..fe439c7 100644 --- a/templates/scaffold-hbar-clip-wallet/template.json +++ b/templates/scaffold-hbar-clip-wallet/template.json @@ -1,12 +1,18 @@ { "name": "scaffold-hbar-clip-wallet", "version": "0.1.0", - "description": "Your own non-custodial wallet on the Clip Wallet kit: a branded browser extension (14 network families, 1Mask for every dapp, decoded approvals, a security floor that can't be switched off) plus a Scaffold-HBAR Next.js dapp that connects to it on Hedera testnet, with the debug page. Testnet by default; mainnet behind a checklist. Same project as `npx create-clip-wallet`.", + "description": "Your own non-custodial wallet on the Clip Wallet kit, on every platform from one clip.config.ts: a branded browser extension, a desktop app (macOS, Windows, Linux) and a phone app (iOS, Android) (14 network families, 1Mask for every dapp, decoded approvals, a security floor that can't be switched off), plus a Scaffold-HBAR Next.js dapp that connects to it on Hedera testnet, with the debug page. Testnet by default; mainnet behind a checklist. Same project as `npx create-clip-wallet --scaffold-hbar`.", "create-scaffold-hbar": { "capabilities": { - "frontend": ["nextjs-app"], - "solidityFramework": ["none"], - "packageManager": ["none"] + "frontend": [ + "nextjs-app" + ], + "solidityFramework": [ + "none" + ], + "packageManager": [ + "none" + ] }, "defaults": { "frontend": "nextjs-app", @@ -14,13 +20,15 @@ "packageManager": "none" }, "requirements": { - "node": ">=22", + "node": ">=22.18", "pnpm": ">=10" }, "rename": { "clip-wallet-project": { "to": "{{projectName}}", - "paths": ["."] + "paths": [ + "." + ] } }, "outro": { @@ -29,29 +37,62 @@ { "title": "Give the wallet its own identity", "steps": [ - { "label": "Install (pnpm manages this template)", "command": "pnpm install" }, { - "label": "Name, reverse domain you own, extension key, icon, listing drafts", + "label": "Install (pnpm manages this template)", + "command": "pnpm install" + }, + { + "label": "Name, reverse domain you own, app id, extension key, icons for every platform, listing drafts", "command": "pnpm wallet:identity --name \"My Wallet\" --rdns com.example.mywallet" }, - { "text": "Same result as `npx create-clip-wallet`. Add --walletconnect-project-id for your own WalletConnect project." } + { + "text": "Same result as `npx create-clip-wallet --scaffold-hbar`. Add --logo logo.png for your own icons and --walletconnect-project-id for your own WalletConnect project." + } ] }, { "title": "Build and try it on Hedera testnet", "steps": [ - { "label": "Build the extension", "command": "pnpm extension:build" }, - { "text": "Load packages/extension/.output/chrome-mv3 unpacked in chrome://extensions, then create a wallet in it." }, - { "label": "Start the demo dapp (connect, sign, send, /debug)", "command": "pnpm next:dev" }, - { "label": "Before every commit", "command": "pnpm harness" } + { + "label": "The browser extension", + "command": "pnpm dev:extension" + }, + { + "text": "Or pnpm extension:build and load packages/extension/.output/chrome-mv3 unpacked in chrome://extensions; then create a wallet in it." + }, + { + "label": "The desktop app (macOS, Windows, Linux)", + "command": "pnpm dev:desktop" + }, + { + "label": "The phone app (iOS, Android): Metro for a development build", + "command": "pnpm --filter mobile start" + }, + { + "label": "Start the demo dapp (connect, sign, send, /debug)", + "command": "pnpm next:dev" + }, + { + "label": "Before every commit", + "command": "pnpm harness" + } ] }, { "title": "Later", "steps": [ - { "text": "Listing-submission drafts for your identity are in docs/listings (pnpm wallet:listings regenerates them)." }, - { "text": "Mainnet stays off until packages/extension/MAINNET.md is done: pnpm wallet:mainnet-check shows what is left." }, - { "text": "AGENTS.md has recipes for rebranding, networks, tokens, routing, security and plugins." } + { + "text": "Installers and store builds: pnpm desktop:dist, pnpm mobile:prebuild or EAS; accounts and signing in docs/signing.md." + }, + { + "text": "Listing-submission drafts for your identity are in docs/listings (pnpm wallet:listings regenerates them)." + }, + { + "text": "Mainnet stays off until MAINNET.md is done: pnpm wallet:mainnet-check shows what is left." + }, + { + "text": "AGENTS.md has recipes for rebranding, platforms, networks, tokens, routing, security and plugins." + } ] } ] diff --git a/templates/scaffold-hbar-clip-wallet/tools/harness/check.mjs b/templates/scaffold-hbar-clip-wallet/tools/harness/check.mjs index 76762a0..616b6fe 100644 --- a/templates/scaffold-hbar-clip-wallet/tools/harness/check.mjs +++ b/templates/scaffold-hbar-clip-wallet/tools/harness/check.mjs @@ -12,8 +12,9 @@ * mistakes agents actually make: importing key-material libraries outside the vault, importing the * vault from the wrong place, logging secrets, committing .env files or key files, losing the vault's test vectors. * - * In a wallet built on the kit (packages/extension/clip.config.ts, no packages/vault) it also checks the wallet's own - * identity, the security floor, the mainnet checklist and that kit packages are pinned (kitChecks below). + * In a wallet built on the kit (clip.config.ts and wallet.identity.json at the root, no packages/vault) it also checks + * the wallet's own identity, that every platform builds through its kit (the security floor), the mainnet checklist and + * that kit packages are pinned (kitChecks below). */ import { execFileSync } from "node:child_process"; import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; @@ -481,7 +482,7 @@ export function runChecks({ root, tracked, skipPaths = SKIP_PATHS, wordlistFrom fail("env-tracked", f, 0, `${f} is tracked by git. .env files hold secrets: run \`git rm --cached ${f}\` and keep it in .gitignore.`); } if (/\.pem$/.test(base) || /(?:^|\/)\.keys\//.test(f)) { - fail("key-file-tracked", f, 0, `${f} is tracked by git. It looks like a private key (the extension's signing key lives in packages/extension/.keys): run \`git rm --cached ${f}\` and keep it out of git.`); + fail("key-file-tracked", f, 0, `${f} is tracked by git. It looks like a private key (the extension's signing key lives in .keys/): run \`git rm --cached ${f}\` and keep it out of git.`); } } } @@ -519,24 +520,29 @@ export function runChecks({ root, tracked, skipPaths = SKIP_PATHS, wordlistFrom /** Clip Wallet's own identity, which no kit-built wallet may announce. */ export const CLIP_WALLET_RDNS = "org.coldai.clipwallet"; export const KIT_EXTENSION = "packages/extension"; +export const KIT_DESKTOP = "packages/desktop"; +export const KIT_MOBILE = "packages/mobile"; export const KIT_PACKAGES = /^(?:@clip-wallet\/[\w-]+|create-clip-wallet)$/; const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[\w.]+)?$/; -/** A wallet made from the template: its extension is packages/extension with clip.config.ts, and there is no vault source. */ +/** + * A wallet made from the template or create-clip-wallet: one clip.config.ts and wallet.identity.json at the root for + * every platform (packages/extension, packages/desktop, packages/mobile), and no vault source. + */ export function isKitProject(root) { - return existsSync(join(root, KIT_EXTENSION, "clip.config.ts")) && !existsSync(join(root, "packages/vault")); + return existsSync(join(root, "clip.config.ts")) && existsSync(join(root, "wallet.identity.json")) && !existsSync(join(root, "packages/vault")); } function kitChecks(root, files, parsed, fail, warn) { const ext = KIT_EXTENSION; // kit-identity: the wallet announces its own identity, never Clip Wallet's, and never carries a private key. - const idFile = `${ext}/wallet.identity.json`; + const idFile = "wallet.identity.json"; let id; try { const text = readFileSync(join(root, idFile), "utf8"); if (/PRIVATE KEY/.test(text)) { - fail("kit-identity", idFile, 0, "wallet.identity.json contains a private key. Only the public key belongs here (extension.key); the private key stays in packages/extension/.keys, out of git."); + fail("kit-identity", idFile, 0, "wallet.identity.json contains a private key. Only the public key belongs here (extension.key); the private key stays in .keys/, out of git."); } id = JSON.parse(text); } catch { @@ -546,6 +552,9 @@ function kitChecks(root, files, parsed, fail, warn) { if (id.rdns === CLIP_WALLET_RDNS || /^org\.coldai\./.test(String(id.rdns))) { fail("kit-identity", idFile, 0, `rdns ${id.rdns} belongs to Clip Wallet. Announce your own: \`pnpm wallet:identity --rdns
\`.`); } + if (/^org\.coldai\./.test(String(id.appId ?? ""))) { + fail("kit-identity", idFile, 0, `appId ${id.appId} belongs to Clip Wallet. Use your own: \`pnpm wallet:identity --id \`.`); + } if (String(id.name).trim().toLowerCase() === "clip wallet") { fail("kit-identity", idFile, 0, "The name Clip Wallet is taken. Give your wallet its own: `pnpm wallet:identity --name \"…\"`."); } @@ -554,15 +563,27 @@ function kitChecks(root, files, parsed, fail, warn) { } } - // kit-security: the build goes through clipWallet(), which has no switch for the security floor. - const wxtFile = `${ext}/wxt.config.ts`; - const wxt = parsed.get(wxtFile); - const usesKit = - wxt && - findImports(wxt.src, wxt.lexed).some((i) => i.module === "@clip-wallet/extension-kit/wxt" && /\bclipWallet\b/.test(i.clause)) && - /\bclipWallet\s*\(/.test(wxt.lexed.code); - if (!usesKit) { - fail("kit-security", wxtFile, 0, "wxt.config.ts must build the extension with clipWallet() from @clip-wallet/extension-kit/wxt: it carries the security floor and the mainnet checklist. Restore it from the template."); + // kit-security: every platform's build goes through its kit (clipWallet(), clipDesktop(), expoConfig() + + // withClipWallet()), none of which has a switch for the security floor, and all of which enforce the mainnet checklist. + const builds = [ + { dir: ext, file: `${ext}/wxt.config.ts`, module: "@clip-wallet/extension-kit/wxt", fn: "clipWallet", what: "the extension" }, + { dir: KIT_DESKTOP, file: `${KIT_DESKTOP}/electron.vite.config.ts`, module: "@clip-wallet/desktop-kit/electron-vite", fn: "clipDesktop", what: "the desktop app" }, + { dir: KIT_DESKTOP, file: `${KIT_DESKTOP}/electron-builder.config.cjs`, module: "@clip-wallet/desktop-kit/builder", fn: "electronBuilderConfig", what: "the desktop installers" }, + { dir: KIT_MOBILE, file: `${KIT_MOBILE}/app.config.ts`, module: "@clip-wallet/mobile-kit/expo", fn: "expoConfig", what: "the phone app's Expo config" }, + { dir: KIT_MOBILE, file: `${KIT_MOBILE}/metro.config.js`, module: "@clip-wallet/mobile-kit/metro", fn: "withClipWallet", what: "the phone app's bundle" }, + ]; + for (const b of builds) { + if (!existsSync(join(root, b.dir, "package.json"))) continue; + const f = parsed.get(b.file); + // Code with comments stripped (for the call); the raw text for a CommonJS require (strings are blanked in `code`). + const raw = f ? f.src : existsSync(join(root, b.file)) ? readFileSync(join(root, b.file), "utf8") : ""; + const code = f ? f.lexed.code : raw; + const imported = + (f && findImports(f.src, f.lexed).some((i) => i.module === b.module && new RegExp(`\\b${b.fn}\\b`).test(i.clause))) || + new RegExp(`\\b${b.fn}\\b[^;]*=\\s*require\\(\\s*["']${b.module.replace(/\//g, "\\/")}["']\\s*\\)`).test(raw); + if (!imported || !new RegExp(`\\b${b.fn}\\s*\\(`).test(code)) { + fail("kit-security", b.file, 0, `${b.file.split("/").pop()} must build ${b.what} with ${b.fn}() from ${b.module}: it carries the wallet's config, the security floor and the mainnet checklist. Restore it from the template.`); + } } for (const [f, { src, lexed }] of parsed) { if (!f.startsWith("packages/")) continue; @@ -573,11 +594,11 @@ function kitChecks(root, files, parsed, fail, warn) { } // kit-mainnet: mainnet on means every box in MAINNET.md is ticked. - const cfgFile = `${ext}/clip.config.ts`; + const cfgFile = "clip.config.ts"; const cfg = parsed.get(cfgFile); const on = cfg && /\bmainnet\s*:\s*\{/.exec(cfg.lexed.code); if (on) { - const listFile = `${ext}/MAINNET.md`; + const listFile = "MAINNET.md"; const list = existsSync(join(root, listFile)) ? readFileSync(join(root, listFile), "utf8") : ""; const open = list.split(/\r?\n/).filter((l) => /^\s*- \[ \]/.test(l)).length; if (!list) fail("kit-mainnet", listFile, 0, "Mainnet is on in clip.config.ts but MAINNET.md is missing. Restore it from the template and work through it."); diff --git a/templates/scaffold-hbar-clip-wallet/tools/verify-provenance.mjs b/templates/scaffold-hbar-clip-wallet/tools/verify-provenance.mjs index acfcb4d..a8a093c 100644 --- a/templates/scaffold-hbar-clip-wallet/tools/verify-provenance.mjs +++ b/templates/scaffold-hbar-clip-wallet/tools/verify-provenance.mjs @@ -14,8 +14,8 @@ * * Node built-ins only. Network: registry.npmjs.org. */ -import { existsSync, readFileSync } from "node:fs"; -import { join } from "node:path"; +import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const root = join(fileURLToPath(new URL(".", import.meta.url)), ".."); @@ -27,20 +27,28 @@ function read(file) { return JSON.parse(readFileSync(join(root, file), "utf8")); } -/** The kit packages this project runs, at the version it pins. */ +/** The kit packages this project runs (every platform's, and what the kits depend on), at the version it pins. */ function packages() { - const ext = read("packages/extension/package.json"); - const version = ext.dependencies?.["@clip-wallet/extension-kit"]; - if (!version || !/^\d+\.\d+\.\d+(?:-[\w.]+)?$/.test(version)) { - throw new Error("packages/extension/package.json must pin @clip-wallet/extension-kit to one exact version."); + const files = ["package.json", ...readdirSync(join(root, "packages"), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => `packages/${d.name}/package.json`)]; + /** @type {Map} */ + const pinned = new Map(); + const kits = []; + for (const f of files.filter((x) => existsSync(join(root, x)))) { + const pkg = read(f); + for (const [name, range] of Object.entries({ ...pkg.dependencies, ...pkg.devDependencies })) { + if (!/^(?:@clip-wallet\/|create-clip-wallet$)/.test(name)) continue; + if (!/^\d+\.\d+\.\d+(?:-[\w.]+)?$/.test(String(range))) throw new Error(`${f} must pin ${name} to one exact version (it has "${range}").`); + pinned.set(name, String(range)); + if (/-kit$/.test(name)) kits.push(join(root, dirname(f), "node_modules", name, "package.json")); + } } - const names = new Set(["@clip-wallet/extension-kit", "@clip-wallet/config"]); - const kit = join(root, "packages/extension/node_modules/@clip-wallet/extension-kit/package.json"); - if (existsSync(kit)) for (const d of Object.keys(JSON.parse(readFileSync(kit, "utf8")).dependencies ?? {})) if (d.startsWith("@clip-wallet/")) names.add(d); - const list = [...names].map((name) => ({ name, version })); - const cli = read("package.json").devDependencies?.["create-clip-wallet"]; - if (cli) list.push({ name: "create-clip-wallet", version: cli }); - return list; + const version = pinned.get("@clip-wallet/config") ?? [...pinned.values()][0]; + if (!version) throw new Error("No @clip-wallet/* package is pinned in this project."); + for (const kit of kits) { + if (!existsSync(kit)) continue; + for (const d of Object.keys(JSON.parse(readFileSync(kit, "utf8")).dependencies ?? {})) if (d.startsWith("@clip-wallet/") && !pinned.has(d)) pinned.set(d, version); + } + return [...pinned].map(([name, v]) => ({ name, version: v })); } async function json(url) { diff --git a/templates/scaffold-hbar-clip-wallet/packages/extension/wallet.identity.json b/templates/scaffold-hbar-clip-wallet/wallet.identity.json similarity index 100% rename from templates/scaffold-hbar-clip-wallet/packages/extension/wallet.identity.json rename to templates/scaffold-hbar-clip-wallet/wallet.identity.json diff --git a/tools/harness/check.mjs b/tools/harness/check.mjs index 76762a0..616b6fe 100644 --- a/tools/harness/check.mjs +++ b/tools/harness/check.mjs @@ -12,8 +12,9 @@ * mistakes agents actually make: importing key-material libraries outside the vault, importing the * vault from the wrong place, logging secrets, committing .env files or key files, losing the vault's test vectors. * - * In a wallet built on the kit (packages/extension/clip.config.ts, no packages/vault) it also checks the wallet's own - * identity, the security floor, the mainnet checklist and that kit packages are pinned (kitChecks below). + * In a wallet built on the kit (clip.config.ts and wallet.identity.json at the root, no packages/vault) it also checks + * the wallet's own identity, that every platform builds through its kit (the security floor), the mainnet checklist and + * that kit packages are pinned (kitChecks below). */ import { execFileSync } from "node:child_process"; import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; @@ -481,7 +482,7 @@ export function runChecks({ root, tracked, skipPaths = SKIP_PATHS, wordlistFrom fail("env-tracked", f, 0, `${f} is tracked by git. .env files hold secrets: run \`git rm --cached ${f}\` and keep it in .gitignore.`); } if (/\.pem$/.test(base) || /(?:^|\/)\.keys\//.test(f)) { - fail("key-file-tracked", f, 0, `${f} is tracked by git. It looks like a private key (the extension's signing key lives in packages/extension/.keys): run \`git rm --cached ${f}\` and keep it out of git.`); + fail("key-file-tracked", f, 0, `${f} is tracked by git. It looks like a private key (the extension's signing key lives in .keys/): run \`git rm --cached ${f}\` and keep it out of git.`); } } } @@ -519,24 +520,29 @@ export function runChecks({ root, tracked, skipPaths = SKIP_PATHS, wordlistFrom /** Clip Wallet's own identity, which no kit-built wallet may announce. */ export const CLIP_WALLET_RDNS = "org.coldai.clipwallet"; export const KIT_EXTENSION = "packages/extension"; +export const KIT_DESKTOP = "packages/desktop"; +export const KIT_MOBILE = "packages/mobile"; export const KIT_PACKAGES = /^(?:@clip-wallet\/[\w-]+|create-clip-wallet)$/; const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[\w.]+)?$/; -/** A wallet made from the template: its extension is packages/extension with clip.config.ts, and there is no vault source. */ +/** + * A wallet made from the template or create-clip-wallet: one clip.config.ts and wallet.identity.json at the root for + * every platform (packages/extension, packages/desktop, packages/mobile), and no vault source. + */ export function isKitProject(root) { - return existsSync(join(root, KIT_EXTENSION, "clip.config.ts")) && !existsSync(join(root, "packages/vault")); + return existsSync(join(root, "clip.config.ts")) && existsSync(join(root, "wallet.identity.json")) && !existsSync(join(root, "packages/vault")); } function kitChecks(root, files, parsed, fail, warn) { const ext = KIT_EXTENSION; // kit-identity: the wallet announces its own identity, never Clip Wallet's, and never carries a private key. - const idFile = `${ext}/wallet.identity.json`; + const idFile = "wallet.identity.json"; let id; try { const text = readFileSync(join(root, idFile), "utf8"); if (/PRIVATE KEY/.test(text)) { - fail("kit-identity", idFile, 0, "wallet.identity.json contains a private key. Only the public key belongs here (extension.key); the private key stays in packages/extension/.keys, out of git."); + fail("kit-identity", idFile, 0, "wallet.identity.json contains a private key. Only the public key belongs here (extension.key); the private key stays in .keys/, out of git."); } id = JSON.parse(text); } catch { @@ -546,6 +552,9 @@ function kitChecks(root, files, parsed, fail, warn) { if (id.rdns === CLIP_WALLET_RDNS || /^org\.coldai\./.test(String(id.rdns))) { fail("kit-identity", idFile, 0, `rdns ${id.rdns} belongs to Clip Wallet. Announce your own: \`pnpm wallet:identity --rdns \`.`); } + if (/^org\.coldai\./.test(String(id.appId ?? ""))) { + fail("kit-identity", idFile, 0, `appId ${id.appId} belongs to Clip Wallet. Use your own: \`pnpm wallet:identity --id \`.`); + } if (String(id.name).trim().toLowerCase() === "clip wallet") { fail("kit-identity", idFile, 0, "The name Clip Wallet is taken. Give your wallet its own: `pnpm wallet:identity --name \"…\"`."); } @@ -554,15 +563,27 @@ function kitChecks(root, files, parsed, fail, warn) { } } - // kit-security: the build goes through clipWallet(), which has no switch for the security floor. - const wxtFile = `${ext}/wxt.config.ts`; - const wxt = parsed.get(wxtFile); - const usesKit = - wxt && - findImports(wxt.src, wxt.lexed).some((i) => i.module === "@clip-wallet/extension-kit/wxt" && /\bclipWallet\b/.test(i.clause)) && - /\bclipWallet\s*\(/.test(wxt.lexed.code); - if (!usesKit) { - fail("kit-security", wxtFile, 0, "wxt.config.ts must build the extension with clipWallet() from @clip-wallet/extension-kit/wxt: it carries the security floor and the mainnet checklist. Restore it from the template."); + // kit-security: every platform's build goes through its kit (clipWallet(), clipDesktop(), expoConfig() + + // withClipWallet()), none of which has a switch for the security floor, and all of which enforce the mainnet checklist. + const builds = [ + { dir: ext, file: `${ext}/wxt.config.ts`, module: "@clip-wallet/extension-kit/wxt", fn: "clipWallet", what: "the extension" }, + { dir: KIT_DESKTOP, file: `${KIT_DESKTOP}/electron.vite.config.ts`, module: "@clip-wallet/desktop-kit/electron-vite", fn: "clipDesktop", what: "the desktop app" }, + { dir: KIT_DESKTOP, file: `${KIT_DESKTOP}/electron-builder.config.cjs`, module: "@clip-wallet/desktop-kit/builder", fn: "electronBuilderConfig", what: "the desktop installers" }, + { dir: KIT_MOBILE, file: `${KIT_MOBILE}/app.config.ts`, module: "@clip-wallet/mobile-kit/expo", fn: "expoConfig", what: "the phone app's Expo config" }, + { dir: KIT_MOBILE, file: `${KIT_MOBILE}/metro.config.js`, module: "@clip-wallet/mobile-kit/metro", fn: "withClipWallet", what: "the phone app's bundle" }, + ]; + for (const b of builds) { + if (!existsSync(join(root, b.dir, "package.json"))) continue; + const f = parsed.get(b.file); + // Code with comments stripped (for the call); the raw text for a CommonJS require (strings are blanked in `code`). + const raw = f ? f.src : existsSync(join(root, b.file)) ? readFileSync(join(root, b.file), "utf8") : ""; + const code = f ? f.lexed.code : raw; + const imported = + (f && findImports(f.src, f.lexed).some((i) => i.module === b.module && new RegExp(`\\b${b.fn}\\b`).test(i.clause))) || + new RegExp(`\\b${b.fn}\\b[^;]*=\\s*require\\(\\s*["']${b.module.replace(/\//g, "\\/")}["']\\s*\\)`).test(raw); + if (!imported || !new RegExp(`\\b${b.fn}\\s*\\(`).test(code)) { + fail("kit-security", b.file, 0, `${b.file.split("/").pop()} must build ${b.what} with ${b.fn}() from ${b.module}: it carries the wallet's config, the security floor and the mainnet checklist. Restore it from the template.`); + } } for (const [f, { src, lexed }] of parsed) { if (!f.startsWith("packages/")) continue; @@ -573,11 +594,11 @@ function kitChecks(root, files, parsed, fail, warn) { } // kit-mainnet: mainnet on means every box in MAINNET.md is ticked. - const cfgFile = `${ext}/clip.config.ts`; + const cfgFile = "clip.config.ts"; const cfg = parsed.get(cfgFile); const on = cfg && /\bmainnet\s*:\s*\{/.exec(cfg.lexed.code); if (on) { - const listFile = `${ext}/MAINNET.md`; + const listFile = "MAINNET.md"; const list = existsSync(join(root, listFile)) ? readFileSync(join(root, listFile), "utf8") : ""; const open = list.split(/\r?\n/).filter((l) => /^\s*- \[ \]/.test(l)).length; if (!list) fail("kit-mainnet", listFile, 0, "Mainnet is on in clip.config.ts but MAINNET.md is missing. Restore it from the template and work through it."); diff --git a/tools/harness/test/check.test.mjs b/tools/harness/test/check.test.mjs index 831cb46..3097e2f 100644 --- a/tools/harness/test/check.test.mjs +++ b/tools/harness/test/check.test.mjs @@ -158,26 +158,43 @@ test("kit-built wallets: Clip Wallet's identity, a switched-off security floor, try { cpSync(join(repo, "templates", "scaffold-hbar-clip-wallet"), dir, { recursive: true }); const ext = join(dir, "packages", "extension"); - writeFileSync(join(ext, "wallet.identity.json"), JSON.stringify({ name: "Clip Wallet", rdns: "org.coldai.clipwallet", icon: "./icon.svg" })); + writeFileSync(join(dir, "wallet.identity.json"), JSON.stringify({ name: "Clip Wallet", rdns: "org.coldai.clipwallet", appId: "org.coldai.clipwallet", icon: "./icon.svg" })); writeFileSync(join(ext, "wxt.config.ts"), 'import { defineConfig } from "wxt";\nexport default defineConfig({ srcDir: "src" });\n'); writeFileSync(join(ext, "src", "security.ts"), "export const threat = {\n openLists: false,\n};\n"); - const cfg = join(ext, "clip.config.ts"); + // The desktop and phone apps must build through their kits too. + writeFileSync(join(dir, "packages", "desktop", "electron.vite.config.ts"), 'import { defineConfig } from "electron-vite";\nexport default defineConfig({});\n'); + writeFileSync(join(dir, "packages", "mobile", "metro.config.js"), 'const { getDefaultConfig } = require("expo/metro-config");\n// withClipWallet( is only mentioned here\nmodule.exports = getDefaultConfig(__dirname);\n'); + const cfg = join(dir, "clip.config.ts"); const on = readFileSync(cfg, "utf8").replace(/^(\s*)mainnet: false,$/m, "$1mainnet: { enabled: true, acknowledged: MAINNET_ACKNOWLEDGEMENT },"); writeFileSync(cfg, on); const mainnetLine = on.split("\n").findLastIndex((l) => l.includes("mainnet: { enabled")) + 1; const pkg = join(ext, "package.json"); writeFileSync(pkg, readFileSync(pkg, "utf8").replace('"@clip-wallet/extension-kit": "0.1.0"', '"@clip-wallet/extension-kit": "^0.1.0"')); - const r = runChecks({ root: dir, tracked: ["packages/extension/.keys/extension.pem"], wordlistFrom: repo }); + const r = runChecks({ root: dir, tracked: [".keys/extension.pem"], wordlistFrom: repo }); assert.deepEqual(where(r), [ - "key-file-tracked packages/extension/.keys/extension.pem:0", - "kit-identity packages/extension/wallet.identity.json:0", - "kit-identity packages/extension/wallet.identity.json:0", - `kit-mainnet packages/extension/clip.config.ts:${mainnetLine}`, + "key-file-tracked .keys/extension.pem:0", + "kit-identity wallet.identity.json:0", + "kit-identity wallet.identity.json:0", + "kit-identity wallet.identity.json:0", + `kit-mainnet clip.config.ts:${mainnetLine}`, "kit-pinned packages/extension/package.json:0", + "kit-security packages/desktop/electron.vite.config.ts:0", "kit-security packages/extension/src/security.ts:2", "kit-security packages/extension/wxt.config.ts:0", + "kit-security packages/mobile/metro.config.js:0", ]); } finally { rmSync(dir, { recursive: true, force: true }); } }); + +test("kit-built wallets: a project with only some platforms checks only those", () => { + const dir = mkdtempSync(join(tmpdir(), "clip-kit-")); + try { + cpSync(join(repo, "templates", "scaffold-hbar-clip-wallet"), dir, { recursive: true }); + for (const p of ["extension", "desktop", "nextjs"]) rmSync(join(dir, "packages", p), { recursive: true, force: true }); + assert.deepEqual(runChecks({ root: dir, tracked: [], wordlistFrom: repo }).failures, []); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/tools/kit/e2e.mjs b/tools/kit/e2e.mjs index b7ce13b..227af63 100644 --- a/tools/kit/e2e.mjs +++ b/tools/kit/e2e.mjs @@ -1,25 +1,30 @@ #!/usr/bin/env node /** - * pnpm kit:e2e: a kit-built wallet from the published shape, end to end, in a temp directory. + * pnpm kit:e2e: a kit-built wallet on every platform, from the published shape, end to end, in a temp directory. * * 1. pnpm pack-all (build, manifest checks, tarballs in .packs; nothing published) - * 2. install the packed create-clip-wallet and run it: `create-clip-wallet acme-wallet --name … --rdns … --yes` - * 3. the project has its own identity: wallet.identity.json, extension key (0600, gitignored, untracked), icons, - * page titles, WalletConnect id in .env only, listing drafts for every enabled registry + * 2. install the packed create-clip-wallet and run it non-interactively for every platform (extension, desktop, + * mobile) with a custom name, rdns, app id, accent, PNG logo, networks and languages + * 3. the project has its own identity: wallet.identity.json at the root, extension key (0600, gitignored, untracked), + * every platform's icons rendered from the logo, WalletConnect id in .env only, listing drafts * 4. point the project's kit dependencies at the tarballs (pnpm overrides), `pnpm install` - * 5. `pnpm build` in packages/extension succeeds; the manifest carries the name, key and icons - * 6. `pnpm harness`, `pnpm check-types` and `pnpm wallet:listings` pass inside the project - * 7. Chromium loads the built extension: its id is the one the key fixes, and a page sees it announce itself over - * EIP-6963 with the new name and rdns (and nothing announces Clip Wallet) - * 8. the mainnet checklist is enforced: mainnet on → the harness, the build and wallet:mainnet-check all refuse - * 9. the security floor: `openLists: false` anywhere fails the harness - * 10. with --dapp: the Scaffold-HBAR dapp builds too + * 5. `pnpm harness` and `pnpm check-types` (every platform) inside the project + * 6. extension: `pnpm extension:build`; the manifest carries the name, key and icons; Chromium loads it: its id is + * the one the key fixes, and a page sees it announce itself over EIP-6963 with the new name and rdns + * 7. desktop: `pnpm desktop:build`, then electron-builder for this machine (macOS arm64: .app + zip); the bundle id, + * name, deep-link scheme and icon are the wallet's; the packaged app starts headless and shows onboarding + * 8. mobile: `expo export` (iOS and Android JS bundles) with the wallet's config inlined, and `expo prebuild` (native + * projects, no pod install): bundle id, package, name, scheme and icons are the wallet's + * 9. the mainnet checklist is enforced by the harness, every platform's build and wallet:mainnet-check + * 10. the security floor: `openLists: false` anywhere fails the harness + * 11. with --dapp: a second project with --scaffold-hbar, whose Next.js dapp builds * - * node tools/kit/e2e.mjs [--no-pack] [--dapp] [--keep] + * node tools/kit/e2e.mjs [--no-pack] [--dapp] [--keep] [--skip-desktop] [--skip-mobile] */ import { createRequire } from "node:module"; import { createServer } from "node:http"; -import { existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { existsSync, lstatSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import { ROOT, check, git, installCli, json, ok, packs, run, step, tempDir, useLocalPacks } from "./lib.mjs"; @@ -29,77 +34,138 @@ const work = tempDir("clip-kit-e2e-"); const WC = "0123456789abcdef0123456789abcdef"; const NAME = "Acme Wallet"; const RDNS = "com.acme.wallet"; +const APP_ID = "com.acme.app"; const NETWORKS = "evm:*,hedera,solana,bitcoin,ton,near,stellar,tezos,algorand"; +const LANGUAGES = "en,de,ja,ar"; +const pw = createRequire(join(ROOT, "apps", "extension", "package.json"))("@playwright/test"); + +/** Sizes of build outputs, for the report. */ +const sizes = []; +function du(path) { + const st = lstatSync(path); + if (st.isSymbolicLink()) return 0; + if (!st.isDirectory()) return st.size; + return readdirSync(path).reduce((s, f) => s + du(join(path, f)), 0); +} +function record(what, path) { + const bytes = du(path); + sizes.push([what, path, bytes]); + ok(`${what}: ${(bytes / 1024 / 1024).toFixed(1)} MB`); +} + +/** A 1024×1024 logo with transparency (an accent ring on nothing), PNG, drawn with the CLI's own encoder. */ +async function writeLogo(cliDir, file) { + const { canvas, encodePng } = await import(join(cliDir, "node_modules", "create-clip-wallet", "src", "icons.mjs")); + const img = canvas(1024, 1024); + for (let y = 0; y < 1024; y++) { + for (let x = 0; x < 1024; x++) { + const d = Math.hypot(x - 511.5, y - 511.5); + if (d < 460 && d > 300) img.data.set([0xf5, 0xa6, 0x23, 255], (y * 1024 + x) * 4); + else if (d < 120) img.data.set([0xff, 0xff, 0xff, 255], (y * 1024 + x) * 4); + } + } + writeFileSync(file, encodePng(img)); +} + +/** A key of an XML plist (Info.plist), as plain text. */ +function plistValue(xml, key) { + const m = new RegExp(`${key}\\s*([^<]*)`).exec(xml); + return m?.[1]; +} try { step("Pack every publishable package"); const list = packs({ repack: !args.includes("--no-pack") }); - check(list.length >= 32 && list.some((p) => p.name === "@clip-wallet/extension-kit") && list.some((p) => p.name === "create-clip-wallet"), `${list.length} tarballs in .packs`); + for (const n of ["@clip-wallet/extension-kit", "@clip-wallet/desktop-kit", "@clip-wallet/mobile-kit", "create-clip-wallet"]) { + if (!list.some((p) => p.name === n)) throw new Error(`${n} is not in .packs`); + } + check(list.length >= 34, `${list.length} tarballs in .packs (extension-kit, desktop-kit, mobile-kit, create-clip-wallet among them)`); - step("create-clip-wallet from its tarball"); - const cli = installCli(join(work, "cli-install"), list); + step("create-clip-wallet from its tarball, every platform, non-interactive"); + const cliDir = join(work, "cli-install"); + const cli = installCli(cliDir, list); const tarballFiles = run("tar", ["-tzf", list.find((p) => p.name === "create-clip-wallet").path], { quiet: true }).out; check(/package\/template\/template\.json/.test(tarballFiles) && /package\/template\/_gitignore/.test(tarballFiles), "the tarball bundles the template (dotfiles as _gitignore)"); - run(process.execPath, [cli, "acme-wallet", "--name", NAME, "--rdns", RDNS, "--accent", "#0B7A3B", "--networks", NETWORKS, "--homepage", "https://wallet.acme.example", "--walletconnect-project-id", WC, "--yes"], { cwd: work }); + check(/package\/template\/packages\/desktop\/electron\.vite\.config\.ts/.test(tarballFiles) && /package\/template\/packages\/mobile\/app\.config\.ts/.test(tarballFiles), "…with the desktop and mobile apps"); + const logo = join(work, "acme-logo.png"); + await writeLogo(cliDir, logo); + const created = run( + process.execPath, + [cli, "acme-wallet", "--name", NAME, "--rdns", RDNS, "--id", APP_ID, "--accent", "#0B7A3B", "--logo", logo, "--networks", NETWORKS, "--languages", LANGUAGES, "--homepage", "https://wallet.acme.example", "--walletconnect-project-id", WC, "--yes"], + { cwd: work, quiet: true }, + ).out; + process.stdout.write(created.split("\n").map((l) => ` | ${l}`).join("\n") + "\n"); const app = join(work, "acme-wallet"); const ext = join(app, "packages", "extension"); + const desk = join(app, "packages", "desktop"); + const mob = join(app, "packages", "mobile"); + check(["pnpm dev:extension", "pnpm dev:desktop", "pnpm --filter mobile start", "pnpm desktop:dist", "pnpm mobile:export"].every((s) => created.includes(s)), "next steps for each platform"); + check(JSON.stringify(readdirSync(join(app, "packages")).sort()) === JSON.stringify(["desktop", "extension", "mobile"]), "packages: extension, desktop, mobile (no dapp without --scaffold-hbar)"); - step("The wallet has its own identity"); - const id = json(join(ext, "wallet.identity.json")); - check(id.name === NAME && id.rdns === RDNS && id.homepage === "https://wallet.acme.example", "wallet.identity.json: name, rdns, homepage"); + step("The wallet has its own identity, on every platform"); + const id = json(join(app, "wallet.identity.json")); + check(id.name === NAME && id.rdns === RDNS && id.appId === APP_ID && id.homepage === "https://wallet.acme.example" && id.icon === "./icon.png", "wallet.identity.json: name, rdns, app id, homepage, logo"); check(/^[A-Za-z0-9+/=]{300,}$/.test(id.extension?.key ?? ""), "extension.key is a public key (base64 SPKI)"); - const { extensionIdFromKey } = await import(join(app, "..", "cli-install", "node_modules", "create-clip-wallet", "src", "identity.mjs")); + const { extensionIdFromKey } = await import(join(cliDir, "node_modules", "create-clip-wallet", "src", "identity.mjs")); const extId = extensionIdFromKey(id.extension.key); - const pem = join(ext, ".keys", "extension.pem"); - check(existsSync(pem) && (statSync(pem).mode & 0o077) === 0, "private key in packages/extension/.keys/extension.pem, mode 0600"); + const pem = join(app, ".keys", "extension.pem"); + check(existsSync(pem) && (statSync(pem).mode & 0o077) === 0, "private key in .keys/extension.pem, mode 0600"); check(!git(app, "ls-files").split("\n").some((f) => /\.pem$|\.keys\/|(^|\/)\.env(\.local)?$/.test(f)), "no key or .env file is tracked by git"); - check(readFileSync(join(ext, ".env"), "utf8").includes(`CLIP_WALLETCONNECT_PROJECT_ID=${WC}`), "WalletConnect project id in packages/extension/.env"); - check(!readFileSync(join(ext, "clip.config.ts"), "utf8").includes(WC), "…and not in clip.config.ts"); - check(readFileSync(join(ext, "src/entrypoints/popup/index.html"), "utf8").includes(`${NAME}`), "page titles use the name"); + check(readFileSync(join(app, ".env"), "utf8").includes(`CLIP_WALLETCONNECT_PROJECT_ID=${WC}`), "WalletConnect project id in .env"); + const cfgText = readFileSync(join(app, "clip.config.ts"), "utf8"); + check(!cfgText.includes(WC) && cfgText.includes('languages: ["en", "de", "ja", "ar"]') && cfgText.includes('accent: "#0B7A3B"'), "clip.config.ts: accent and languages set, no WalletConnect id"); + check(readFileSync(join(ext, "src/entrypoints/popup/index.html"), "utf8").includes(`${NAME}`), "extension page titles use the name"); + check(readFileSync(join(app, "icon.png")).equals(readFileSync(logo)), "icon.png is the logo"); + const { decodePng, readIcns, readIco } = await import(join(cliDir, "node_modules", "create-clip-wallet", "src", "icons.mjs")); + const px = (f) => decodePng(readFileSync(f)); + check([16, 32, 48, 128].every((s) => px(join(ext, `public/icon/${s}.png`)).width === s), "extension icons 16/32/48/128"); + const icns = readIcns(readFileSync(join(desk, "build/icon.icns"))); + check(icns.length === 10 && icns.some((e) => e.type === "ic10" && decodePng(e.png).width === 1024), `macOS icon.icns: ${icns.map((e) => e.type).join(" ")}`); + check(readIco(readFileSync(join(desk, "build/icon.ico"))).map((e) => e.size).join() === "16,24,32,48,64,128,256", "Windows icon.ico: 16–256"); + check(readdirSync(join(desk, "build/icons")).length === 8, "Linux icons: 16–1024"); + const iosIcon = px(join(mob, "assets/icon.png")); + check(iosIcon.width === 1024 && iosIcon.data[3] === 255 && iosIcon.data[0] === 0x0b && iosIcon.data[1] === 0x7a, "iOS icon: 1024, opaque, the logo on the accent colour"); + check(px(join(mob, "assets/adaptive-icon.png")).data[3] === 0, "Android adaptive foreground: transparent around the logo"); const listings = readdirSync(join(app, "docs", "listings")).sort(); check( JSON.stringify(listings) === JSON.stringify(["README.md", "beacon.md", "eip-6963.md", "near.md", "stellar-wallets-kit.md", "ton-connect.md", "use-wallet.md", "walletconnect-explorer.md"]), `listing drafts: ${listings.join(", ")}`, ); - const tc = readFileSync(join(app, "docs/listings/ton-connect.md"), "utf8"); - check(tc.includes('"app_name": "acmewallet"') && tc.includes('"key": "acmewallet"'), "TON Connect entry uses the wallet key"); - check(readFileSync(join(app, "docs/listings/beacon.md"), "utf8").includes(`id: '${extId}'`), "Beacon entry uses the extension id"); - check(readFileSync(join(app, "docs/listings/walletconnect-explorer.md"), "utf8").includes(`"injected_id": "${RDNS}"`), "WalletConnect Explorer entry uses the rdns"); step("Install from the tarballs"); useLocalPacks(app, list); run("pnpm", ["install"], { cwd: app }); + const installed = JSON.parse(readFileSync(join(desk, "node_modules", "@clip-wallet", "desktop-kit", "package.json"), "utf8")); + check(!JSON.stringify(installed.exports).includes("development"), "installed kits are the packed (dist) shape, not workspace sources"); - step("pnpm build (the extension)"); - run("pnpm", ["build"], { cwd: ext }); - const out = join(ext, ".output", "chrome-mv3"); - const manifest = json(join(out, "manifest.json")); - check(manifest.name === NAME && manifest.action?.default_title === NAME, "manifest name and action title"); - check(manifest.key === id.extension.key, "manifest key = wallet.identity.json extension.key"); - check(["16", "32", "48", "128"].every((s) => existsSync(join(out, manifest.icons[s]))), "manifest icons exist"); - check(manifest.description.endsWith("Test networks only."), "testnet build says so"); - - step("pnpm harness, check-types and wallet:listings inside the project"); + step("pnpm harness and check-types (every platform) inside the project"); const harness = run("pnpm", ["harness"], { cwd: app, quiet: true }); check(/harness: ok/.test(harness.out), "pnpm harness passes"); - run("pnpm", ["extension:check-types"], { cwd: app }); - ok("extension types"); + run("pnpm", ["check-types"], { cwd: app }); + ok("types: extension, desktop, mobile"); run("pnpm", ["wallet:listings"], { cwd: app }); check(git(app, "status", "--porcelain", "docs/listings").trim() === "", "wallet:listings regenerates the same drafts"); - step("Chromium loads it: own extension id, own EIP-6963 announcement"); - const { chromium } = createRequire(join(ROOT, "apps", "extension", "package.json"))("@playwright/test"); + step("Extension: build, manifest, Chromium loads it with its own id and EIP-6963 announcement"); + run("pnpm", ["extension:build"], { cwd: app }); + const out = join(ext, ".output", "chrome-mv3"); + const manifest = json(join(out, "manifest.json")); + check(manifest.name === NAME && manifest.action?.default_title === NAME, "manifest name and action title"); + check(manifest.key === id.extension.key, "manifest key = wallet.identity.json extension.key"); + check(["16", "32", "48", "128"].every((s) => readFileSync(join(out, manifest.icons[s])).equals(readFileSync(join(ext, "public", manifest.icons[s])))), "manifest icons are the rendered ones"); + check(manifest.description.endsWith("Test networks only."), "testnet build says so"); + record("extension (chrome-mv3, unpacked)", out); + run("pnpm", ["extension:zip"], { cwd: app, quiet: true }); + const zip = readdirSync(join(ext, ".output")).find((f) => f.endsWith("-chrome.zip")); + check(zip === "acme-wallet-0.1.0-chrome.zip", `store zip named after the wallet: ${zip}`); + record("extension store zip", join(ext, ".output", zip)); const server = createServer((_req, res) => { res.writeHead(200, { "content-type": "text/html" }); res.end("dappdapp"); }); await new Promise((r) => server.listen(0, "127.0.0.1", r)); const port = server.address().port; - const context = await chromium.launchPersistentContext("", { - channel: "chromium", - headless: true, - args: [`--disable-extensions-except=${out}`, `--load-extension=${out}`], - }); + const context = await pw.chromium.launchPersistentContext("", { channel: "chromium", headless: true, args: [`--disable-extensions-except=${out}`, `--load-extension=${out}`] }); try { let [sw] = context.serviceWorkers(); if (!sw) sw = await context.waitForEvent("serviceworker"); @@ -116,8 +182,7 @@ try { }), ); check(announced.length === 1 && announced[0].name === NAME && announced[0].rdns === RDNS, `EIP-6963 announcement: ${JSON.stringify(announced.map((a) => ({ name: a.name, rdns: a.rdns })))}`); - check(/^data:image\/svg\+xml;base64,/.test(announced[0].icon), "announced icon is the wallet's SVG"); - check(!announced.some((a) => a.rdns === "org.coldai.clipwallet"), "nothing announces Clip Wallet"); + check(announced[0].icon === `data:image/png;base64,${readFileSync(logo).toString("base64")}`, "announced icon is the wallet's logo"); const globals = await page.evaluate(() => ({ key: typeof window.acmewallet?.near, clip: typeof window.clipwallet })); check(globals.key === "object" && globals.clip === "undefined", "NEAR/Stellar/Algorand providers hang off window.acmewallet, not window.clipwallet"); const popup = await context.newPage(); @@ -128,36 +193,142 @@ try { server.close(); } - step("The mainnet checklist is enforced"); - const cfgPath = join(ext, "clip.config.ts"); - const cfg = readFileSync(cfgPath, "utf8"); + if (!args.includes("--skip-desktop")) { + step("Desktop: electron-vite build, electron-builder for this machine, the packaged app starts"); + run("pnpm", ["desktop:build"], { cwd: app }); + const html = readFileSync(join(desk, "out/renderer/wallet/index.html"), "utf8"); + check(html.includes(`${NAME}`) && /Content-Security-Policy" content="default-src 'none'/.test(html), "wallet page: the wallet's name and CSP"); + const mainJs = readdirSync(join(desk, "out/main"), { recursive: true }).filter((f) => String(f).endsWith(".mjs")).map((f) => readFileSync(join(desk, "out/main", String(f)), "utf8")).join("\n"); + check(mainJs.includes(`"${RDNS}"`) && mainJs.includes(`"${NAME}"`) && !mainJs.includes("org.coldai.clipwallet"), "main process carries the wallet's config (and not Clip Wallet's)"); + check(existsSync(join(desk, "out/native-host/clip-native-host.cjs")), "native-messaging host bundled"); + record("desktop out/ (main, preloads, renderer)", join(desk, "out")); + const host = process.platform === "darwin" ? ["--mac", "zip", `--${process.arch}`] : process.platform === "win32" ? ["--win", "zip"] : ["--linux", "tar.gz"]; + run("pnpm", ["exec", "electron-builder", ...host, "--config", "electron-builder.config.cjs", "--publish", "never"], { cwd: desk, env: { CSC_IDENTITY_AUTO_DISCOVERY: "false" } }); + const release = join(desk, "release"); + const artifacts = readdirSync(release).filter((f) => /\.(zip|tar\.gz|exe|dmg)$/.test(f)); + check(artifacts.some((f) => f.startsWith("Acme-Wallet-0.1.0-")), `installer artifact: ${artifacts.join(", ")}`); + for (const a of artifacts) record(`desktop ${a}`, join(release, a)); + if (process.platform === "darwin") { + const appDir = readdirSync(release).find((d) => d.startsWith("mac")); + const bundle = join(release, appDir, `${NAME}.app`); + check(existsSync(bundle), `${appDir}/${NAME}.app`); + record(`desktop ${NAME}.app (${appDir})`, bundle); + const plist = run("plutil", ["-convert", "xml1", "-o", "-", join(bundle, "Contents", "Info.plist")], { quiet: true }).out; + check(plistValue(plist, "CFBundleIdentifier") === `${APP_ID}.desktop`, `CFBundleIdentifier ${APP_ID}.desktop`); + check(plistValue(plist, "CFBundleName") === NAME && plistValue(plist, "CFBundleDisplayName") === NAME, "CFBundleName / CFBundleDisplayName"); + check(/CFBundleURLSchemes<\/key>\s*\s*acmewallet<\/string>/.test(plist), "deep-link scheme acmewallet:// registered"); + const iconFile = plistValue(plist, "CFBundleIconFile"); + check(readFileSync(join(bundle, "Contents", "Resources", iconFile.endsWith(".icns") ? iconFile : `${iconFile}.icns`)).equals(readFileSync(join(desk, "build/icon.icns"))), "the app icon is the rendered icon.icns"); + const exe = join(bundle, "Contents", "MacOS", NAME); + const home = mkdtempSync(join(tmpdir(), "clip-kit-desktop-")); + let electronApp; + for (let attempt = 1; !electronApp; attempt++) { + try { + electronApp = await pw._electron.launch({ executablePath: exe, args: [`--user-data-dir=${home}`, "--use-mock-keychain"], env: { ...process.env, CLIP_DESKTOP_NO_SYSTEM_INTEGRATION: "1" }, timeout: 30_000 }); + } catch (e) { + if (attempt >= 3) throw e; + } + } + try { + const win = await electronApp.firstWindow(); + await win.waitForURL(/^clip-app:\/\/wallet\/wallet\//, { timeout: 30_000 }); + await win.getByRole("button", { name: "Create a new wallet" }).waitFor({ timeout: 30_000 }); + check((await win.title()) === NAME, `packaged app: wallet window titled ${NAME}, onboarding shown`); + const isolated = await win.evaluate(() => typeof window.clipDesktop === "object" && typeof window.require === "undefined" && typeof window.process === "undefined"); + check(isolated, "the wallet window sees only the bridge (no Node)"); + const appName = await electronApp.evaluate(({ app: a }) => a.getName()); + check(appName === NAME, `app.getName() = ${appName}`); + } finally { + await electronApp.close().catch(() => undefined); + rmSync(home, { recursive: true, force: true }); + } + } + } + + if (!args.includes("--skip-mobile")) { + step("Mobile: expo config, expo export (iOS + Android JS bundles), expo prebuild (native projects)"); + const cfg = JSON.parse(run("pnpm", ["exec", "expo", "config", "--type", "public", "--json"], { cwd: mob, quiet: true }).out.replace(/^[^{]*/, "")); + check(cfg.name === NAME && cfg.slug === "acme-wallet" && cfg.scheme === "acmewallet", `Expo config: name ${cfg.name}, slug ${cfg.slug}, scheme ${cfg.scheme}`); + check(cfg.ios?.bundleIdentifier === APP_ID && cfg.android?.package === APP_ID, `bundle id / package ${cfg.ios?.bundleIdentifier} / ${cfg.android?.package}`); + check(cfg.android?.adaptiveIcon?.backgroundColor === "#0B7A3B" && cfg.icon === "./assets/icon.png", "icons: icon.png, adaptive icon on the accent colour"); + run("pnpm", ["mobile:export"], { cwd: app, env: { CI: "1" } }); + const dist = join(mob, "dist"); + const meta = json(join(dist, "metadata.json")); + const bundles = ["ios", "android"].map((p) => join(dist, meta.fileMetadata[p].bundle)); + check(bundles.every((b) => existsSync(b)), `JS bundles: ${bundles.map((b) => b.slice(dist.length + 1)).join(", ")}`); + for (const [p, b] of [["ios", bundles[0]], ["android", bundles[1]]]) record(`mobile ${p} bundle (Hermes)`, b); + record("mobile dist/ (expo export)", dist); + const iosBundle = readFileSync(bundles[0]); + // (1Mask's inpage script keeps Clip Wallet's identity as its built-in default; the app always passes its own.) + check(iosBundle.includes(Buffer.from(RDNS)) && iosBundle.includes(Buffer.from(NAME)), "the bundle carries the wallet's resolved config"); + run("pnpm", ["exec", "expo", "prebuild", "--no-install", "--clean"], { cwd: mob, env: { CI: "1", EXPO_NO_GIT_STATUS: "1" } }); + const iosDir = readdirSync(join(mob, "ios")).find((d) => d.endsWith(".xcodeproj"))?.replace(/\.xcodeproj$/, ""); + check(!!iosDir, `ios/${iosDir}.xcodeproj generated`); + const pbx = readFileSync(join(mob, "ios", `${iosDir}.xcodeproj`, "project.pbxproj"), "utf8"); + check(new RegExp(`PRODUCT_BUNDLE_IDENTIFIER = "?${APP_ID.replace(/\./g, "\\.")}"?;`).test(pbx), `iOS bundle id ${APP_ID}`); + const info = readFileSync(join(mob, "ios", iosDir, "Info.plist"), "utf8"); + check(plistValue(info, "CFBundleDisplayName") === NAME && /acmewallet<\/string>/.test(info), "iOS display name and URL scheme"); + check(/Unlock Acme Wallet with Face ID/.test(info), "iOS permission texts use the name"); + const appIcon = join(mob, "ios", iosDir, "Images.xcassets", "AppIcon.appiconset"); + const iconPng = readdirSync(appIcon).find((f) => f.endsWith(".png")); + check(!!iconPng && px(join(appIcon, iconPng)).width === 1024, `iOS AppIcon (${iconPng}) from assets/icon.png`); + const gradle = readFileSync(join(mob, "android", "app", "build.gradle"), "utf8"); + check(gradle.includes(`applicationId '${APP_ID}'`) || gradle.includes(`applicationId "${APP_ID}"`), `Android applicationId ${APP_ID}`); + const strings = readFileSync(join(mob, "android", "app", "src", "main", "res", "values", "strings.xml"), "utf8"); + check(strings.includes(`${NAME}`), "Android app_name"); + const manifestXml = readFileSync(join(mob, "android", "app", "src", "main", "AndroidManifest.xml"), "utf8"); + check(manifestXml.includes('android:scheme="acmewallet"'), "Android deep-link scheme"); + const res = join(mob, "android", "app", "src", "main", "res"); + check(readdirSync(res).some((d) => d.startsWith("mipmap-") && readdirSync(join(res, d)).some((f) => /ic_launcher_foreground|ic_launcher\./.test(f))), "Android launcher icons from the adaptive icon"); + check(readFileSync(join(res, "values", "colors.xml"), "utf8").includes("#0B7A3B"), "Android icon / splash background is the accent"); + } + + step("The mainnet checklist is enforced on every platform"); + const cfgPath = join(app, "clip.config.ts"); writeFileSync( cfgPath, - cfg + cfgText .replace('import { defineConfig } from "@clip-wallet/config";', 'import { MAINNET_ACKNOWLEDGEMENT, defineConfig } from "@clip-wallet/config";') .replace(/^ {2}mainnet: false,$/m, " mainnet: { enabled: true, acknowledged: MAINNET_ACKNOWLEDGEMENT },"), ); - const h = run("pnpm", ["harness"], { cwd: app, allowFail: true, quiet: true }); - check(h.code !== 0 && /kit-mainnet/.test(h.out), "pnpm harness refuses mainnet with open MAINNET.md boxes"); - const b = run("pnpm", ["build"], { cwd: ext, allowFail: true, quiet: true }); - check(b.code !== 0 && /mainnet checklist: MAINNET\.md: /.test(b.out), "the extension build refuses mainnet while MAINNET.md has open boxes (identity, keys and services are all set here)"); - const m = run("pnpm", ["wallet:mainnet-check"], { cwd: app, allowFail: true, quiet: true }); - check(m.code === 1 && /Mainnet is ON but not ready/.test(m.out), "wallet:mainnet-check exits 1: mainnet on, not ready"); - writeFileSync(cfgPath, cfg); + try { + const h = run("pnpm", ["harness"], { cwd: app, allowFail: true, quiet: true }); + check(h.code !== 0 && /kit-mainnet/.test(h.out), "pnpm harness refuses mainnet with open MAINNET.md boxes"); + const b = run("pnpm", ["extension:build"], { cwd: app, allowFail: true, quiet: true }); + check(b.code !== 0 && /mainnet checklist: MAINNET\.md: /.test(b.out), "the extension build refuses it"); + if (!args.includes("--skip-desktop")) { + const d = run("pnpm", ["desktop:build"], { cwd: app, allowFail: true, quiet: true }); + check(d.code !== 0 && /mainnet checklist: MAINNET\.md: /.test(d.out), "the desktop build refuses it"); + } + if (!args.includes("--skip-mobile")) { + const m = run("pnpm", ["exec", "expo", "config", "--type", "public"], { cwd: mob, allowFail: true, quiet: true }); + check(m.code !== 0 && /mainnet checklist: MAINNET\.md: /.test(m.out), "the phone app's Expo config refuses it"); + } + const c = run("pnpm", ["wallet:mainnet-check"], { cwd: app, allowFail: true, quiet: true }); + check(c.code === 1 && /Mainnet is ON but not ready/.test(c.out), "wallet:mainnet-check exits 1: mainnet on, not ready"); + } finally { + writeFileSync(cfgPath, cfgText); + } step("The security floor can't be switched off"); - const sneaky = join(ext, "src", "sneaky.ts"); + const sneaky = join(mob, "sneaky.ts"); writeFileSync(sneaky, "export const threat = { openLists: false };\n"); const s = run("pnpm", ["harness"], { cwd: app, allowFail: true, quiet: true }); check(s.code !== 0 && /kit-security/.test(s.out), "pnpm harness fails on openLists: false"); rmSync(sneaky); if (args.includes("--dapp")) { - step("The Scaffold-HBAR dapp builds"); - run("pnpm", ["next:build"], { cwd: app }); + step("With --scaffold-hbar: the Scaffold-HBAR dapp builds"); + run(process.execPath, [cli, "dapp-wallet", "--platforms", "extension", "--scaffold-hbar", "--name", "Dapp Wallet", "--yes"], { cwd: work, quiet: true }); + const dapp = join(work, "dapp-wallet"); + useLocalPacks(dapp, list); + run("pnpm", ["install"], { cwd: dapp }); + run("pnpm", ["next:build"], { cwd: dapp }); ok("next build"); } + process.stdout.write("\nBuild outputs:\n"); + for (const [what, path, bytes] of sizes) process.stdout.write(` ${what.padEnd(48)} ${(bytes / 1024 / 1024).toFixed(1).padStart(7)} MB ${path.replace(work, "")}\n`); process.stdout.write(`\nkit e2e: all checks passed${keep ? ` (kept ${work})` : ""}\n`); } catch (e) { process.stderr.write(`\nkit e2e FAILED: ${e instanceof Error ? e.message : String(e)}\n(work dir kept: ${work})\n`); diff --git a/tools/kit/scaffold-hbar-e2e.mjs b/tools/kit/scaffold-hbar-e2e.mjs index d735c47..92b70e9 100644 --- a/tools/kit/scaffold-hbar-e2e.mjs +++ b/tools/kit/scaffold-hbar-e2e.mjs @@ -43,13 +43,13 @@ try { ).out; process.stdout.write(out.split("\n").slice(-40).join("\n")); const app = join(work, "app"); - check(existsSync(join(app, "packages/extension/clip.config.ts")) && !existsSync(join(app, "template.json")), "project created; template.json consumed"); + check(existsSync(join(app, "clip.config.ts")) && existsSync(join(app, "packages/desktop/electron.vite.config.ts")) && !existsSync(join(app, "template.json")), "project created (every platform); template.json consumed"); check(JSON.parse(readFileSync(join(app, "package.json"), "utf8")).name === "app", "template.json rename map applied (package name = folder)"); check(/pnpm wallet:identity/.test(out), "the outro comes from template.json (pnpm commands)"); - step("The same wallet with create-clip-wallet (from its tarball)"); + step("The same wallet with create-clip-wallet --scaffold-hbar (from its tarball)"); const cli = installCli(join(work, "cli-install"), list); - run(process.execPath, [cli, join(work, "cli", "app"), "--name", "Fresh Scaffold", "--rdns", "com.example.freshscaffold", "--yes"], { cwd: work }); + run(process.execPath, [cli, join(work, "cli", "app"), "--scaffold-hbar", "--name", "Fresh Scaffold", "--rdns", "com.example.freshscaffold", "--yes"], { cwd: work }); step("Both projects install the packed kit"); useLocalPacks(app, list); diff --git a/tools/release/normalize-manifests.mjs b/tools/release/normalize-manifests.mjs index 6f449f0..d89e8b1 100644 --- a/tools/release/normalize-manifests.mjs +++ b/tools/release/normalize-manifests.mjs @@ -59,8 +59,9 @@ export const EXTRA = { "@clip-wallet/media-client": { description: "Decides what an untrusted NFT media URL may become; shared by the wallet UI and the media proxy" }, "@clip-wallet/kit-modules": { description: "Clip Wallet modules for ecosystem wallet pickers: NEAR Wallet Selector, Stellar Wallets Kit, Beacon, use-wallet" }, "create-clip-wallet": { - description: "Scaffold your own wallet on the Clip Wallet kit: its own name, icon, extension id and rdns, testnet by default", - keywords: ["clip-wallet", "wallet", "scaffold-hbar", "hedera", "create", "template", "browser-extension"], + description: + "Scaffold your own wallet on the Clip Wallet kit, on every platform from one clip.config.ts: browser extension, desktop app (Electron) and phone app (Expo), with its own name, ids and icons; testnet by default", + keywords: ["clip-wallet", "wallet", "scaffold-hbar", "hedera", "create", "template", "browser-extension", "electron", "expo", "desktop", "mobile"], }, }; const FAMILY = { diff --git a/tools/release/sync-template.mjs b/tools/release/sync-template.mjs index 17b96d9..5f6bf62 100644 --- a/tools/release/sync-template.mjs +++ b/tools/release/sync-template.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node /** * Keep the Scaffold-HBAR template (templates/scaffold-hbar-clip-wallet) in step with the kit: - * - the kit packages it pins (@clip-wallet/* in packages/extension, create-clip-wallet at the root) are the version + * - the kit packages it pins (@clip-wallet/* in every platform package and at the root, create-clip-wallet) are the version * the monorepo is about to publish (all published packages share one version, .changeset/config.json "fixed"); * - tools/harness/check.mjs is the monorepo's harness, byte for byte. * Runs after `changeset version` (pnpm version-packages). `--check` only reports (pnpm harness runs it). @@ -33,6 +33,8 @@ function pin(file, fields) { } pin("package.json", ["devDependencies"]); pin("packages/extension/package.json", ["dependencies", "devDependencies"]); +pin("packages/desktop/package.json", ["dependencies", "devDependencies"]); +pin("packages/mobile/package.json", ["dependencies", "devDependencies"]); // The demo dapp uses Clip Connect (@clip-wallet/connect) for its Clip Connect page. pin("packages/nextjs/package.json", ["dependencies", "devDependencies"]); From 2f04a52b7844f463bd62f20e85034cc04c05643d Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 20:40:22 +0800 Subject: [PATCH 3/6] docs(kit): launching a wallet on every platform; READMEs and agent docs for the platform kits - Docs site (apps/docs/src/kit): the "launch your own wallet" pages cover every platform. index (the platforms, the project layout with the config, identity, logo, MAINNET.md, .env and .keys/ at the root), create-clip-wallet (options, prompts, next steps, what it writes, icons from one logo, commands, the Scaffold-HBAR route, how kit:e2e tests it), config (platform settings: appId, desktop/mobile ids, scheme, languages, fees/usage reserved), build-and-ship (extension with configDir, desktop app, phone app, what the harness checks) and a new signing page (store accounts and code-signing variable names, never values) in the sidebar. Snippets for the project's wxt.config.ts, electron.vite.config.ts and app.config.ts are typechecked against the kits; the snippet test knows the kit project's scripts; reference.ts documents the new clip.config settings. - Repo layout, vault-importer rules and the engine diagram follow the kits; @module headers on the new entry files for the API reference; pako's declarations are referenced from gunzip.ts so other programs typecheck it. - create-clip-wallet, @clip-wallet/config, desktop-kit and mobile-kit READMEs (Install, Example, Documentation); apps/desktop and apps/mobile READMEs point at the kits; AGENTS.md (layout, commands, a desktop/phone recipe, the template recipe) and llms.txt. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 45 ++++-- apps/desktop/README.md | 10 +- apps/docs/.vitepress/sidebar.ts | 1 + apps/docs/package.json | 2 + apps/docs/scripts/reference.ts | 12 ++ apps/docs/src/architecture/engine.md | 4 +- apps/docs/src/guide/repo-layout.md | 8 +- apps/docs/src/guide/rules.md | 3 +- apps/docs/src/kit/build-and-ship.md | 73 +++++++++- apps/docs/src/kit/config.md | 50 +++++-- apps/docs/src/kit/create-clip-wallet.md | 137 ++++++++++++++---- apps/docs/src/kit/index.md | 64 +++++--- apps/docs/src/kit/scaffold-hbar.md | 13 +- apps/docs/src/kit/signing.md | 91 ++++++++++++ .../snippets/kit/{ => project}/clip.config.ts | 2 +- .../packages/desktop/electron.vite.config.ts | 6 + .../packages/extension}/wxt.config.ts | 5 +- .../kit/project/packages/mobile/app.config.ts | 4 + apps/docs/test/snippets.test.ts | 17 ++- apps/mobile/README.md | 8 +- llms.txt | 22 +-- packages/config/README.md | 21 ++- packages/config/src/node.ts | 2 + packages/create-clip-wallet/README.md | 118 +++++++++++---- packages/desktop-kit/README.md | 28 +++- packages/desktop-kit/src/builder.ts | 2 + packages/desktop-kit/src/csp.ts | 2 + packages/desktop-kit/src/electron-vite.ts | 2 + packages/desktop-kit/src/index.ts | 2 + packages/desktop-kit/src/main/index.ts | 2 + packages/desktop-kit/src/native-host/main.ts | 2 + packages/desktop-kit/src/preload/dapp.ts | 2 + packages/desktop-kit/src/preload/wallet.ts | 2 + packages/desktop-kit/src/renderer/index.ts | 2 + packages/mobile-kit/README.md | 22 ++- packages/mobile-kit/src/expo.ts | 2 + packages/mobile-kit/src/index.ts | 2 + packages/mobile-kit/src/plugins/gunzip.ts | 1 + packages/mobile-kit/src/polyfills.ts | 2 + pnpm-lock.yaml | 6 + 40 files changed, 661 insertions(+), 138 deletions(-) create mode 100644 apps/docs/src/kit/signing.md rename apps/docs/src/snippets/kit/{ => project}/clip.config.ts (87%) create mode 100644 apps/docs/src/snippets/kit/project/packages/desktop/electron.vite.config.ts rename apps/docs/src/snippets/kit/{ => project/packages/extension}/wxt.config.ts (56%) create mode 100644 apps/docs/src/snippets/kit/project/packages/mobile/app.config.ts diff --git a/AGENTS.md b/AGENTS.md index 6c53b24..f13c98c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,8 +3,9 @@ Clip Wallet is a non-custodial wallet for every CLPR network: 14 network families, 1Mask for every dapp, decoded approvals, a security floor, features (staking, swaps, on-ramps, Secure Trade), social (contacts, Clip handles, notifications, Discover), sandboxed Clip Plugins, and route-and-fund / settle-on-Hedera on CLPRouter. It is also a -kit: the `@clip-wallet/*` packages, `@clip-wallet/extension-kit` and `create-clip-wallet` let anyone ship their own -wallet. Pre-release: test networks only. +kit: the `@clip-wallet/*` packages, `@clip-wallet/extension-kit`, `@clip-wallet/desktop-kit`, `@clip-wallet/mobile-kit` +and `create-clip-wallet` let anyone ship their own wallet on every platform from one `clip.config.ts`. Pre-release: test +networks only. ## Rules that never break 1. Only `packages/vault` touches seed phrases or private keys. `tools/harness/check.mjs` fails otherwise. @@ -21,20 +22,23 @@ wallet. Pre-release: test networks only. ```bash pnpm install && pnpm typecheck && pnpm test && pnpm harness ``` -Touching the extension: also `pnpm --filter @clip-wallet/extension e2e`. Touching packaging, `create-clip-wallet` or -`templates/`: also `pnpm pack-all && pnpm kit:e2e` (and `pnpm kit:e2e:scaffold-hbar` for the template). +Touching the extension: also `pnpm --filter @clip-wallet/extension e2e`. Touching the desktop app or desktop-kit: also +`pnpm --filter @clip-wallet/desktop e2e`. Touching packaging, `create-clip-wallet`, the kits or `templates/`: also +`pnpm pack-all && pnpm kit:e2e` (every platform from tarballs; and `pnpm kit:e2e:scaffold-hbar` for the template). Release work (Docker for the last one): `pnpm --filter @clip-wallet/extension package`, `actionlint`, `scripts/repro-check.sh`. ## Layout ``` packages/core shared types (the contract); additive changes only -packages/config clip.config.ts schema (zod): identity, theme, networks, route, services, mainnet +packages/config clip.config.ts schema (zod): identity, theme, networks, languages, app ids, scheme, route, services, mainnet; /node loader packages/vault phrase, derivation for 14 families, encryption, approval-bound signing, passkey unlock packages/chains-* evm hedera solana bitcoin sui aptos cardano substrate starknet ton near stellar tezos algorand packages/1mask dapp connectors for all families + WalletConnect; announces the wallet's identity packages/engine environment-free orchestration (approvals, portfolio, catalog, wiring) shared by extension and mobile packages/extension-kit the browser extension as a library: background, pages, WXT config (clipWallet()), security floor +packages/desktop-kit the desktop app as a library: main process, preloads, pages, clipDesktop() (electron-vite), electronBuilderConfig() +packages/mobile-kit the phone app as a library: screens, background host, in-app browser, expoConfig(), withClipWallet() (Metro) packages/ui React screens and theme tokens packages/i18n translation layer (English + 11 languages) packages/route CLPRouter route-and-fund; settle-on-Hedera (Phase 3) client @@ -48,9 +52,10 @@ packages/link linked devices: phone/desktop as signer, encrypted sync packages/connect Clip Connect (@clip-wallet/connect): wallet-agnostic dapp SDK; public standards only, never wallet internals packages/kit-modules modules for ecosystem pickers (NEAR Wallet Selector, Stellar Wallets Kit, use-wallet, Beacon) packages/backup-client, packages/media-client clients for services/backup and services/media-proxy -packages/create-clip-wallet npx create-clip-wallet: the template + identity + listing drafts + mainnet check +packages/create-clip-wallet npx create-clip-wallet: the template, platforms (--platforms), identity, icons from one logo, listing drafts, mainnet check apps/extension Clip Wallet's own extension: identity + one-line entrypoints on extension-kit (e2e lives here) -apps/mobile Expo app on engine +apps/desktop Clip Wallet's own desktop app: identity + one-line entrypoints on desktop-kit (Playwright e2e lives here) +apps/mobile Clip Wallet's own phone app: identity + index.ts / app.config.ts / metro.config.js on mobile-kit services/backup, services/media-proxy, services/link-relay Cloudflare Workers (optional hosted services) brand/ Clip Wallet's brand sources; tools/brand/render.mjs renders the icons contracts/handles ClipHandles on Hedera (Foundry) @@ -109,6 +114,18 @@ Each ends with `pnpm typecheck && pnpm test && pnpm harness`. Product context: ` (`src/globals.ts`); the resolved config is the `virtual:clip-wallet/config` module (`src/config.ts`). 3. Tests in `packages/extension-kit/test` (vitest aliases the virtual module to `test/clip.config.ts`); then `pnpm --filter @clip-wallet/extension e2e`. +### Change the desktop or phone app +1. The code lives in `packages/desktop-kit/src` (main process, preloads, pages) and `packages/mobile-kit/src` (screens, + background host, in-app browser), shared with every kit-built wallet; `apps/desktop` and `apps/mobile` only hold + Clip Wallet's identity, icons and one-line entrypoints. +2. The resolved config is `virtual:clip-wallet/config` (default export + `icon` data URI): `clipDesktop()` in + `packages/desktop-kit/src/electron-vite.ts` provides it to electron-vite, `withClipWallet()` in + `packages/mobile-kit/src/metro.cjs` to Metro; ids and names come from `platformIds()` (`packages/config`), never a + literal. Node-side helpers (`electron-vite.ts`, `builder.ts`, `expo.ts`) have no relative imports: build tools load + them from source. +3. Tests in each kit's `test/` (vitest and, for mobile, jest-expo; both alias the virtual module to `test/clip.config.ts`); + then `pnpm --filter @clip-wallet/desktop e2e` for the desktop app, and `pnpm kit:e2e` (both platforms, from tarballs). + ### Security checks 1. A new threat source implements `ThreatIntelProvider` (`packages/security/src/threat/types.ts`); say in its comment exactly what leaves the device. 2. Anything that sends user data to a third party is off unless a key is configured, and Settings → Security says so. @@ -142,8 +159,14 @@ Each ends with `pnpm typecheck && pnpm test && pnpm harness`. Product context: ` ### Change the Scaffold-HBAR template or create-clip-wallet 1. The template is `templates/scaffold-hbar-clip-wallet` (laid out as create-scaffold-hbar expects: `template.json` - capabilities nextjs-app / no Solidity / pnpm, rename map, outro). create-clip-wallet bundles it at `prepack`. + capabilities nextjs-app / no Solidity / pnpm, rename map, outro). It holds every part: one `clip.config.ts` at the + root, `packages/extension`, `packages/desktop`, `packages/mobile`, `packages/nextjs`. create-clip-wallet bundles it + at `prepack`. 2. Both paths must keep producing the same project: create-clip-wallet copies the template the way create-scaffold-hbar - does (`packages/create-clip-wallet/src/template.mjs`), then runs the identity step that `pnpm wallet:identity` runs. -3. `tools/harness/check.mjs` is copied into the template: `node tools/release/sync-template.mjs` after changing it. -4. Verify: `pnpm --filter create-clip-wallet test`, `pnpm pack-all`, `pnpm kit:e2e`, `pnpm kit:e2e:scaffold-hbar`. + does (`packages/create-clip-wallet/src/template.mjs`), removes the parts that weren't chosen (`selectParts()`: + folders, root scripts, and docs blocks marked `platform:` / `only:`), then runs the identity step that + `pnpm wallet:identity` runs (`--scaffold-hbar` with every platform = the template unchanged). +3. Icons come from one logo through `packages/create-clip-wallet/src/icons.mjs` (Node built-ins only); the files it + writes must stay the ones `electronBuilderConfig()` and `expoConfig()` point at (`test/icons.test.ts` checks). +4. `tools/harness/check.mjs` is copied into the template: `node tools/release/sync-template.mjs` after changing it. +5. Verify: `pnpm --filter create-clip-wallet test`, `pnpm pack-all`, `pnpm kit:e2e`, `pnpm kit:e2e:scaffold-hbar`. diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 0f7e8fd..d0cee3f 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -1,5 +1,11 @@ # Clip Wallet desktop (macOS, Windows, Linux) +> The app's code is [`@clip-wallet/desktop-kit`](../../packages/desktop-kit) (`packages/desktop-kit`), shared with every +> kit-built wallet; this folder is Clip Wallet's brand on it: `clip.config.ts`, the icons rendered from `brand/`, +> `electron.vite.config.ts` (`clipDesktop()`), `electron-builder.config.cjs` (`electronBuilderConfig()`), one-line +> entrypoints, and the Playwright e2e. Paths in the tables below (`src/…`, `scripts/build-inpage.mjs`) are in the kit; +> its unit tests run with `pnpm --filter @clip-wallet/desktop-kit test`. + Electron app with the shared wallet engine in the main process, the extension's screens (`@clip-wallet/ui`) in the wallet and approval windows, and a built-in dapp browser where 1Mask is injected the way the extension injects it. Test networks only (`clip.config.ts`, `mainnet: false`). @@ -7,7 +13,7 @@ Test networks only (`clip.config.ts`, `mainnet: false`). ``` pnpm --filter @clip-wallet/desktop build # electron-vite → out/ (main, sandboxed preloads, renderer) pnpm --filter @clip-wallet/desktop start # run the built app -pnpm --filter @clip-wallet/desktop test # unit tests (IPC schema, origin binding, storage, Touch ID PRF, HID relay, i18n QA) +pnpm --filter @clip-wallet/desktop-kit test # unit tests (IPC schema, origin binding, storage, Touch ID PRF, HID relay, i18n QA, builder mapper) pnpm --filter @clip-wallet/desktop e2e # build + Playwright _electron end-to-end (real vault, testnets) pnpm --filter @clip-wallet/desktop dist:mac # electron-builder: dmg + zip, arm64 + x64 (dist:win, dist:linux, dist:dir) CLIP_EXTENSION_IDS=[,] pnpm … build # Chromium extension ids allowed to use Clip Desktop (native messaging) @@ -17,7 +23,7 @@ CLIP_EXTENSION_IDS=[,] pnpm … build # Chromium extension ids allowed | Piece | Where | Notes | |---|---|---| -| Engine + vault | `src/main/host/wallet.ts` | `@clip-wallet/engine` like the phone (`apps/mobile/src/background/host.ts`); the only place in this app that imports `@clip-wallet/vault` (harness allowlist). Features, Ledger/Keystone, social, security attached as on mobile. | +| Engine + vault | `src/main/host/wallet.ts` | `@clip-wallet/engine` like the phone (`packages/mobile-kit/src/background/host.ts`); the only place in this app that imports `@clip-wallet/vault` (harness allowlist). Features, Ledger/Keystone, social, security attached as on mobile. | | Storage | `src/main/storage.ts` | `vault.json`: the vault record (already Argon2id-sealed) wrapped again with Electron `safeStorage` (Keychain / DPAPI / libsecret or KWallet). `app.json`: public app data. Atomic writes. | | IPC | `src/shared/ipc.ts`, `src/main/ipc-guard.ts`, `src/preload/wallet.ts` | One typed bridge object (`window.clipDesktop`) via `contextBridge`. Main checks zod schema, then the sender (registered window role + top frame + `clip-app://wallet` origin), then the engine's own zod schema. | | Wallet pages | `src/renderer/{wallet,approval}` | Served from `clip-app://wallet/…` (`src/main/app-protocol.ts`) with a strict CSP; only the default session knows the scheme. | diff --git a/apps/docs/.vitepress/sidebar.ts b/apps/docs/.vitepress/sidebar.ts index 6b5067b..8c62d80 100644 --- a/apps/docs/.vitepress/sidebar.ts +++ b/apps/docs/.vitepress/sidebar.ts @@ -102,6 +102,7 @@ const build: Item[] = [ { text: "Scaffold-HBAR template", link: "/kit/scaffold-hbar" }, { text: "Configure clip.config.ts", link: "/kit/config" }, { text: "Build and ship", link: "/kit/build-and-ship" }, + { text: "Stores and code signing", link: "/kit/signing" }, { text: "Work with AI agents", link: "/kit/ai-agents" }, ], }, diff --git a/apps/docs/package.json b/apps/docs/package.json index fe2b120..b6074a6 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -47,6 +47,7 @@ "@clip-wallet/config": "workspace:*", "@clip-wallet/connect": "workspace:*", "@clip-wallet/core": "workspace:*", + "@clip-wallet/desktop-kit": "workspace:*", "@clip-wallet/engine": "workspace:*", "@clip-wallet/extension-kit": "workspace:*", "@clip-wallet/features": "workspace:*", @@ -55,6 +56,7 @@ "@clip-wallet/kit-modules": "workspace:*", "@clip-wallet/link": "workspace:*", "@clip-wallet/media-client": "workspace:*", + "@clip-wallet/mobile-kit": "workspace:*", "@clip-wallet/names": "workspace:*", "@clip-wallet/plugins": "workspace:*", "@clip-wallet/route": "workspace:*", diff --git a/apps/docs/scripts/reference.ts b/apps/docs/scripts/reference.ts index d434da1..b745e44 100644 --- a/apps/docs/scripts/reference.ts +++ b/apps/docs/scripts/reference.ts @@ -70,6 +70,18 @@ const CONFIG_DOCS: Record = { "services.clipHandles.address": "The contract's EVM address (`0x…`).", "services.clipHandles.contractId": "The contract's Hedera id (`0.0.x`).", "services.clipHandles.ledger": "Which Hedera ledger the contract is on.", + languages: "The languages Settings → Language offers, from `LANGUAGES` (`en`, `de`, `fr`, `es`, `pt-BR`, `it`, `tr`, `ja`, `ko`, `zh-Hans`, `ar`, `hi`), each listed once. The first is the fallback. Unset: all twelve.", + appId: "The desktop and phone app id, a reverse-domain id. Unset: `rdns`. The iOS bundle id and Android package are `appId` (hyphens become `_` on Android), the desktop app id `.desktop`; `platformIds(config)` lists them all.", + scheme: "The deep-link URL scheme of the desktop and phone apps (`://wc?uri=…`, `://browse?url=…`). Unset: the wallet key, e.g. `acmewallet`.", + desktop: "Desktop app overrides.", + "desktop.appId": "The desktop app id (macOS bundle id, Windows AppUserModelID). Unset: `.desktop`.", + mobile: "Phone app overrides.", + "mobile.bundleId": "The iOS bundle id. Unset: `appId`.", + "mobile.androidPackage": "The Android package. Unset: `appId`, with hyphens as `_`.", + fees: "Reserved for Clip Cloud's hosted mode. Accepted (other fields are kept), never acted on by the kit. Default: off.", + "fees.enabled": "Reserved; the kit never collects fees, whatever this says.", + usage: "Reserved for Clip Cloud's hosted mode. Accepted (other fields are kept), never acted on by the kit. Default: off.", + "usage.enabled": "Reserved; the kit never reports usage, whatever this says.", mainnet: `\`false\` (default) or the checklist object \`{ enabled: true, acknowledged: MAINNET_ACKNOWLEDGEMENT }\`. A mainnet build also needs \`mainnetProblems(config)\` to be empty.`, }; diff --git a/apps/docs/src/architecture/engine.md b/apps/docs/src/architecture/engine.md index 58fe18a..6a1b1f0 100644 --- a/apps/docs/src/architecture/engine.md +++ b/apps/docs/src/architecture/engine.md @@ -9,8 +9,8 @@ and passkey ceremonies, all on injected seams. flowchart TB subgraph Hosts EXT["Browser extension
extension-kit background
(WalletService)"] - MOB["Phone app
apps/mobile/src/background"] - DESK["Clip Desktop
apps/desktop/src/main/host"] + MOB["Phone app
mobile-kit src/background"] + DESK["Desktop app
desktop-kit src/main/host"] end ENGINE["@clip-wallet/engine
WalletEngine"] WIRING["@clip-wallet/engine/wiring
catalog + the 14 chain modules"] diff --git a/apps/docs/src/guide/repo-layout.md b/apps/docs/src/guide/repo-layout.md index fbcf695..a7257d2 100644 --- a/apps/docs/src/guide/repo-layout.md +++ b/apps/docs/src/guide/repo-layout.md @@ -14,6 +14,8 @@ published to npm as `@clip-wallet/` (plus `create-clip-wallet`), all at on | `packages/1mask` | Dapp connectors for every family, plus WalletConnect. Announces the wallet's identity. | | `packages/engine` | Environment-free orchestration (approvals, permissions, portfolio, catalog) shared by mobile and desktop. | | `packages/extension-kit` | The browser extension as a library: background, pages, WXT build (`clipWallet()`), security floor. | +| `packages/desktop-kit` | The desktop app as a library: main process, sandboxed preloads, pages, built-in dapp browser, electron-vite build (`clipDesktop()`), installers (`electronBuilderConfig()`). | +| `packages/mobile-kit` | The phone app as a library: screens, vault host, in-app dapp browser, plugins, Expo config (`expoConfig()`), Metro wiring (`withClipWallet()`). | | `packages/ui` | React screens and theme tokens. | | `packages/i18n` | The translation layer: English plus 11 languages, and the translation QA checks. | | `packages/route` | Route and fund on CLPRouter; settle-on-Hedera (Phase 3) client. | @@ -34,8 +36,8 @@ published to npm as `@clip-wallet/` (plus `create-clip-wallet`), all at on | Path | What it is | | --- | --- | | `apps/extension` | Clip Wallet's own extension: its identity and one-line entrypoints on `extension-kit`. The Playwright end-to-end tests live here. | -| `apps/mobile` | The Expo app for iOS and Android, on `engine`. | -| `apps/desktop` | The Electron app for macOS, Windows and Linux, on `engine`. | +| `apps/mobile` | Clip Wallet's own phone app (iOS and Android): its identity and one-line entrypoints on `mobile-kit`. | +| `apps/desktop` | Clip Wallet's own desktop app (macOS, Windows and Linux): its identity and one-line entrypoints on `desktop-kit`. The desktop Playwright tests live here. | | `apps/docs` | This site. | | `services/backup`, `services/media-proxy`, `services/link-relay` | Optional Cloudflare Workers. See [Services](../services/). | | `templates/scaffold-hbar-clip-wallet` | The Scaffold-HBAR template (its own workspace, not part of this one). | @@ -55,7 +57,7 @@ flowchart TB chains["chains-*
(14 modules)"] onemask["1mask"] engine["engine"] - kit["extension-kit"] + kit["extension-kit · desktop-kit
mobile-kit"] ui["ui + i18n"] extras["security · features · social
route · plugins · hardware
names · link"] connect["connect
(dapp side)"] diff --git a/apps/docs/src/guide/rules.md b/apps/docs/src/guide/rules.md index 957e980..97c8fbc 100644 --- a/apps/docs/src/guide/rules.md +++ b/apps/docs/src/guide/rules.md @@ -32,7 +32,8 @@ only in: - `packages/vault` itself; - the extension background: `packages/extension-kit/src/background/` and `apps/extension/**/background*`; -- the mobile background (`apps/mobile/src/background/`) and the desktop host (`apps/desktop/src/main/host/`); +- the phone app's background (`packages/mobile-kit/src/background/`) and the desktop host + (`packages/desktop-kit/src/main/host/`); - the onboarding screen (`packages/ui/src/screens/Onboarding.tsx`, or an `onboarding/` folder). Screens talk to the background through messages (`packages/ui/src/client.ts`), never to the vault. diff --git a/apps/docs/src/kit/build-and-ship.md b/apps/docs/src/kit/build-and-ship.md index 8d69511..c3bc6bc 100644 --- a/apps/docs/src/kit/build-and-ship.md +++ b/apps/docs/src/kit/build-and-ship.md @@ -1,12 +1,16 @@ # Build and ship +A project builds each platform from the same `clip.config.ts` at its root. No account or certificate is needed for +anything on this page; signing and store accounts are only for shipping: see [Stores and code signing](./signing.md). + ## The extension's files A wallet project keeps three things; the rest comes from `@clip-wallet/extension-kit`. -**`wxt.config.ts`**: the whole build is `clipWallet()`. +**`wxt.config.ts`**: the whole build is `clipWallet()`. `configDir` points at the project root, where the config, the +icon, `MAINNET.md` and the wallet-wide `.env` live. -<<< @/snippets/kit/wxt.config.ts +<<< @/snippets/kit/project/packages/extension/wxt.config.ts **One-line entrypoints** in `src/entrypoints/`: @@ -42,7 +46,8 @@ pnpm harness && pnpm check-types && pnpm build ## Package for the stores -In a kit-built wallet, `pnpm extension:zip` writes the store zips with WXT. Clip Wallet's own extension in this repo +In a kit-built wallet, `pnpm extension:zip` writes the store zips with WXT, named after the wallet +(`acme-wallet-0.1.0-chrome.zip`). Clip Wallet's own extension in this repo uses a stricter script: ```sh @@ -69,9 +74,65 @@ sends where (Settings → Security and Settings → Your data list it). ## Your extension id The extension id comes from the public key in `extension.key`. Keep the private key -(`packages/extension/.keys/extension.pem`) offline and **use the same key for the store item**, so the id in the store +(`.keys/extension.pem` at the project root) offline and **use the same key for the store item**, so the id in the store matches the one your listings, passkeys and native-messaging hosts know. +## Desktop app + +<<< @/snippets/kit/project/packages/desktop/electron.vite.config.ts + +```sh +pnpm dev:desktop # electron-vite dev +pnpm desktop:build # → packages/desktop/out (main, sandboxed preloads, renderer, native-messaging host) +pnpm desktop:start # run the built app +pnpm desktop:dist # electron-builder for this computer → packages/desktop/release +pnpm desktop:dist:mac # dmg + zip, arm64 and x64 +pnpm desktop:dist:win # NSIS + zip, x64 and arm64 (NSIS: on Windows or in CI) +pnpm desktop:dist:linux # AppImage + deb (on Linux or in CI) + tar.gz (anywhere) +``` + +`clipDesktop()` resolves the config with the build environment (`CLIP_WALLETCONNECT_PROJECT_ID`, `CLIP_UPDATES`, +`CLIP_EXTENSION_IDS`, from the environment or `.env`), gives it to the main process, the preloads and the pages as +`virtual:clip-wallet/config` (the icon inlined as the identity dapps see), fills the pages' title and Content Security +Policy, bundles the native-messaging host, and refuses a mainnet config with open boxes. `electronBuilderConfig()` +(in `electron-builder.config.cjs`) maps the config to electron-builder: app id, product name, executable and artifact +names (`Acme-Wallet-0.1.0-mac-arm64.zip`), the `://` protocol, the Chromium locales for the offered languages, +the icons and the hardened-runtime entitlements, with signing, notarization and update publishing only when their +variables are set. + +The app is the same as Clip Wallet's desktop app: vault and engine in the main process, every renderer sandboxed, a +built-in dapp browser with 1Mask and one session per site, Touch ID, Ledger over WebHID, and linked devices. + +## Phone app + +<<< @/snippets/kit/project/packages/mobile/app.config.ts + +```sh +pnpm mobile:start # Metro for a development build (the app uses native modules: not Expo Go) +pnpm mobile:prebuild # generate ios/ and android/ (never committed; no CocoaPods install) +pnpm --filter mobile ios # build and run on the iOS simulator (Xcode); android: an emulator or a device +pnpm mobile:export # the JavaScript bundles for iOS and Android: no account, no Xcode, no Android SDK +pnpm --filter mobile eas:build # EAS cloud builds (an Expo account) +``` + +`expoConfig()` maps the config to Expo's app config: name, slug, scheme, bundle id, package, icons, the adaptive icon +and the splash on the accent colour, permission texts in the wallet's name, and universal links for +`CLIP_ASSOCIATED_DOMAIN`. `withClipWallet()` (in `metro.config.js`) gives the app the resolved config as +`virtual:clip-wallet/config`, written to `node_modules/.cache/clip-wallet/config.js` on every Metro start. Both refuse a +mainnet config with open boxes. The kit's native modules are peer dependencies: the project lists them, so Expo +autolinking finds them. + +## What the harness checks + +`pnpm harness`, in the project, must pass before every commit: + +- key material only in `@clip-wallet/vault` (inside the kits); no logged secrets; no tracked `.env` or key files; +- the identity is the wallet's own (`rdns` and `appId` never `org.coldai.*`, the name never "Clip Wallet"); +- every platform builds through its kit: `clipWallet()`, `clipDesktop()` and `electronBuilderConfig()`, `expoConfig()` + and `withClipWallet()`; nothing switches the phishing lists off; +- mainnet on means every box in `MAINNET.md` is ticked; +- kit packages are pinned to one exact version. + ## Listings `pnpm wallet:listings` regenerates the drafts in `docs/listings/` for your identity: EIP-6963 metadata, WalletConnect @@ -90,8 +151,8 @@ npm provenance attestation against the kit's public repository; `npm audit signa ::: danger Real funds Clip Wallet is pre-release and has had no external audit. A mainnet build moves real money. The checklist in -`packages/extension/MAINNET.md` is the owner's decision, never an agent's. +`MAINNET.md` (at the project root) is the owner's decision, never an agent's. ::: -`pnpm wallet:mainnet-check` lists what is left. The build refuses mainnet until it is empty: see +`pnpm wallet:mainnet-check` lists what is left. Every platform's build refuses mainnet until it is empty: see [Configure clip.config.ts](./config.md#mainnet). diff --git a/apps/docs/src/kit/config.md b/apps/docs/src/kit/config.md index 0a447bd..251dde4 100644 --- a/apps/docs/src/kit/config.md +++ b/apps/docs/src/kit/config.md @@ -1,11 +1,12 @@ # Configure clip.config.ts -Everything a wallet maker changes, apart from the icon, is one file: `clip.config.ts` (in a kit-built wallet, -`packages/extension/clip.config.ts`; in this repo, `apps/extension/clip.config.ts`). It is validated by -`defineConfig()` from `@clip-wallet/config`, which fills in defaults and refuses anything wrong with a plain sentence -per problem. +Everything a wallet maker changes, apart from the logo, is one file: `clip.config.ts`. In a kit-built wallet it sits at +the project root and drives every platform: the extension, the desktop app and the phone app read the same file. In +this repo, each of Clip Wallet's own apps has one (`apps/extension/clip.config.ts`, `apps/desktop/clip.config.ts`, +`apps/mobile/clip.config.ts`). It is validated by `defineConfig()` from `@clip-wallet/config`, which fills in defaults +and refuses anything wrong with a plain sentence per problem. -<<< @/snippets/kit/clip.config.ts +<<< @/snippets/kit/project/clip.config.ts Only `name` and `rdns` are required. In a kit-built wallet, the identity fields come from `wallet.identity.json`, which `pnpm wallet:identity` writes; edit identity there. Every setting with its type and default: @@ -18,13 +19,39 @@ which `pnpm wallet:identity` writes; edit identity there. Every setting with its | `name` | shown in the stores, wallet pickers and every screen; also derives the window global (`acmewallet`) | | `rdns` | the EIP-6963 id dapps key on: a reverse domain **you own** | | `description`, `homepage` | stores, WalletConnect metadata and listings | -| `icon` | `./icon.svg` or `./icon.png`, shipped inside the extension | +| `icon` | `./icon.svg` or `./icon.png`, next to the config; shipped inside every build as the identity dapps see | | `extension.key` | the public key that fixes the Chrome extension id (written by the identity step) | | `theme` | `accent`, `accentText` (contrast at least 3:1), `font`, `radius` | Screens read only theme tokens and the configured name, so the whole wallet takes your brand. Small accent-coloured text is derived from `accent` to reach 4.5:1 contrast automatically. +## Platforms + +These settings are optional; without them every platform takes its ids from `rdns` and the name. + +| Setting | Default | Extension | Desktop | Phone | +| --- | --- | --- | --- | --- | +| `name`, `description` | | manifest, pickers | window titles, menus, tray, installers, permission prompts | app name, permission texts | +| `rdns` | | EIP-6963 | EIP-6963 in the built-in browser, native-messaging host name | EIP-6963 in the in-app browser, Keychain service `.vault` | +| `appId` | `rdns` | | `.desktop` | iOS bundle id `appId`, Android package (hyphens become `_`) | +| `desktop.appId` | `.desktop` | | override | | +| `mobile.bundleId`, `mobile.androidPackage` | from `appId` | | | overrides | +| `scheme` | the wallet key (`acmewallet`) | | `://wc?uri=…`, `browse`, `link`, `trade` | the same | +| `languages` | all twelve | Settings → Language and device matching | the same, plus the Chromium UI locales kept in the installer | the same | +| `icon` | `./icon.svg` | the identity dapps see | inlined into the build | inlined into the bundle | +| `theme` | | screens | screens | screens; adaptive-icon and splash background are the accent | +| `networks`, `services`, `route`, `hardware`, `passkeys` | | ✓ | ✓ | ✓ | +| `fees`, `usage` | `{ enabled: false }` | reserved for Clip Cloud's hosted mode: accepted, never acted on by the kit | | | +| `mainnet` | `false` | every build refuses it until `MAINNET.md` is done and `mainnetProblems()` is empty | | | + +`languages` takes `en`, `de`, `fr`, `es`, `pt-BR`, `it`, `tr`, `ja`, `ko`, `zh-Hans`, `ar` and `hi`; the first is the +fallback, and only the languages listed are matched and offered. `platformIds(config)` returns every id the platforms +use (extension gecko id, desktop app id, product, executable and artifact names, iOS bundle id, Android package, scheme, +slug), so you can check them before a build. Build tools that need the config outside a bundler (Expo's +`app.config.ts`, electron-builder) use `loadClipConfigSync()` from `@clip-wallet/config/node`, which evaluates the file +in a child Node process (Node 22.18 or later). + ## Networks <<< @/snippets/kit/networks.ts @@ -51,7 +78,7 @@ deployments: [Services](../services/). The WalletConnect project id and partner keys come from the **build environment**, never from this file: ```sh -# packages/extension/.env (gitignored); only CLIP_* lines are read +# .env at the project root (gitignored); only CLIP_* lines are read, by every platform's build CLIP_WALLETCONNECT_PROJECT_ID= CLIP_0X_API_KEY= CLIP_BLOCKAID_API_KEY= @@ -68,7 +95,7 @@ isn't switched on in this build. ::: danger Real funds Clip Wallet is pre-release and has had no external audit. Turning mainnet on makes your wallet move real money. Only do -it after every box in `packages/extension/MAINNET.md` is ticked by a person: your own identity and keys, production +it after every box in `MAINNET.md` (at the project root) is ticked by a person: your own identity and keys, production services, an independent review of your changes, usability tests, and support and security contacts. ::: @@ -76,6 +103,7 @@ services, an independent review of your changes, usability tests, and support an <<< @/snippets/kit/mainnet.ts -Three things enforce it: the extension build refuses mainnet while `mainnetProblems()` lists anything or `MAINNET.md` -has an open box; `pnpm harness` fails while mainnet is on with an open box; and `pnpm wallet:mainnet-check` lists what -is left. Coding agents must never tick the boxes or switch mainnet on. +Three things enforce it: every platform's build (the extension, the desktop app and the phone app) refuses mainnet +while `mainnetProblems()` lists anything or `MAINNET.md` has an open box; `pnpm harness` fails while mainnet is on with +an open box; and `pnpm wallet:mainnet-check` lists what is left. Coding agents must never tick the boxes or switch +mainnet on. diff --git a/apps/docs/src/kit/create-clip-wallet.md b/apps/docs/src/kit/create-clip-wallet.md index 603c300..a1828e4 100644 --- a/apps/docs/src/kit/create-clip-wallet.md +++ b/apps/docs/src/kit/create-clip-wallet.md @@ -1,51 +1,136 @@ # create-clip-wallet -`create-clip-wallet` makes a new wallet project and gives it an identity of its own. +`create-clip-wallet` makes a new wallet project, on every platform you choose, and gives it an identity of its own. ```sh -npx create-clip-wallet my-wallet +npx create-clip-wallet my-wallet # asks for anything you don't pass +npx create-clip-wallet my-wallet --yes # defaults: every platform, starter icon, all networks, all languages ``` -It asks for anything you don't pass as a flag. Without questions: +Without questions: ```sh -npx create-clip-wallet my-wallet --name "Acme Wallet" --rdns com.acme.wallet --accent "#0B7A3B" \ - --networks "evm:*,hedera,solana,bitcoin" --homepage https://wallet.acme.example --yes -cd my-wallet && pnpm install && pnpm extension:build && pnpm next:dev +npx create-clip-wallet acme-wallet \ + --platforms extension,desktop,mobile \ + --name "Acme Wallet" --rdns com.acme.wallet --id com.acme.wallet \ + --logo ./acme-logo.png --accent "#0B7A3B" \ + --networks "evm:*,hedera,solana,bitcoin" --languages en,de,ja \ + --homepage https://wallet.acme.example --walletconnect-project-id <32 hex> \ + --scaffold-hbar --yes ``` -Then load `packages/extension/.output/chrome-mv3` with **Load unpacked** in `chrome://extensions`, create a wallet in -it, and open the dapp at `http://localhost:3000`. +## Options -## What the identity step writes +| Option | | +| --- | --- | +| `--platforms extension,desktop,mobile` | Which apps to make. Default: all three. | +| `--scaffold-hbar` | Also the Scaffold-HBAR demo dapp. Default: off (`--no-scaffold-hbar`). | +| `--name` | The wallet's name on every platform. Default: from the folder name. | +| `--rdns` | EIP-6963 id, a reverse domain you own. Default: `com.example.` (a placeholder the harness warns about). | +| `--id` | Desktop and phone app id (macOS bundle id `.desktop`, iOS bundle id, Android package). Default: the rdns. | +| `--logo` | Your logo: `.png` (1024×1024 is best) or `.svg`. Every icon is rendered from it. Default: a starter mark in the accent colour. Alias: `--icon`. | +| `--accent` | Accent colour (contrast with white text at least 3:1); also the phone app's icon and splash background. | +| `--networks` | `"evm:*"`, `"evm:"`, `hedera`, `solana`, `bitcoin`, `sui`, `aptos`, `cardano`, `substrate`, `starknet`, `ton`, `near`, `stellar`, `tezos`, `algorand`. | +| `--languages` | The languages Settings → Language offers: `en de fr es pt-BR it tr ja ko zh-Hans ar hi`. Default: all; the first is the fallback. | +| `--homepage`, `--description` | Your website and a one-sentence description (stores, pickers, WalletConnect). | +| `--walletconnect-project-id` | Written to `.env`, never committed. | +| `--yes` | No questions. `--no-git` skips `git init`; `--new-key` makes a new extension key (and id). | + +Without `--yes`, it asks for the folder, platforms, the dapp, name, rdns, app id, accent, logo, networks and languages, +and asks again after an answer `@clip-wallet/config` rejects. It never switches mainnet on: there is no `--mainnet`. +Every option and exit code: [create-clip-wallet CLI](../reference/cli.md). + +When it is done it prints the next steps for the platforms you chose: + +```text + cd acme-wallet + pnpm install + + Browser extension (packages/extension) + pnpm dev:extension # Chrome with the extension loaded, live reload + pnpm extension:build # load packages/extension/.output/chrome-mv3 unpacked in chrome://extensions + pnpm extension:zip # the store upload zip + + Desktop app: macOS, Windows, Linux (packages/desktop) + pnpm dev:desktop # run it with live reload + pnpm desktop:dist # installers for this computer in packages/desktop/release (unsigned) + pnpm desktop:dist:mac # also :win and :linux; signing: docs/signing.md + + Phone app: iOS, Android (packages/mobile) + pnpm --filter mobile start # Metro for a development build + pnpm mobile:prebuild # generate ios/ and android/, then pnpm --filter mobile ios (or android) + pnpm mobile:export # the JS bundles, no account needed; EAS: pnpm --filter mobile eas:build + + pnpm harness # must pass before every commit +``` + +## What it writes | File | What | | --- | --- | -| `packages/extension/wallet.identity.json` | name, description, rdns, homepage, icon and `extension.key` (a public key) | -| `packages/extension/.keys/extension.pem` | the extension's **private** key: mode 0600, gitignored, never printed. Its public half fixes the Chrome extension id. Back it up offline. | -| `packages/extension/icon.svg`, `public/icon/*.png` | a starter mark in your accent colour, or your `--icon` | -| `packages/extension/clip.config.ts` | `theme.accent` and `networks`, when given | -| `packages/extension/.env`, `packages/nextjs/.env.local` | your WalletConnect project id (`--walletconnect-project-id`), never committed | +| `clip.config.ts` | the one config for every platform: theme, networks, languages, routing, services, mainnet (`false`) | +| `wallet.identity.json` | name, description, rdns, homepage, icon, appId and `extension.key` (a public key) | +| `.keys/extension.pem` | the extension's **private** key: mode 0600, gitignored, never printed. Its public half fixes the Chrome extension id. Back it up offline. | +| `icon.svg` / `icon.png` and every platform's icons | a starter mark in your accent colour, or your `--logo` (see below) | +| `.env`, `packages/nextjs/.env.local` | your WalletConnect project id (`--walletconnect-project-id`), never committed | | `docs/listings/` | listing drafts for this identity | +| `docs/signing.md` | code signing and store accounts per platform: what each needs and the variable names the tools read | It refuses Clip Wallet's own identity (`org.coldai.*`, the name "Clip Wallet") and anything `@clip-wallet/config` rejects, and then writes nothing. Run again, it keeps the extension key (and so the id) unless you pass `--new-key`. -## Commands +The config, identity, logo, `MAINNET.md`, `.env` and `.keys/` live at the **project root**, shared by every platform, +not in `packages/extension/`. -| Command | In a project | What | -| --- | --- | --- | -| `create-clip-wallet ` | | a new project | -| `create-clip-wallet identity` | `pnpm wallet:identity` | set or change the identity | -| `create-clip-wallet listings` | `pnpm wallet:listings` | regenerate the listing drafts after an identity change | -| `create-clip-wallet mainnet-check` | `pnpm wallet:mainnet-check` | what still blocks a mainnet build (exit `0` ready, `2` work left, `1` mainnet on but not ready) | +## Icons from one logo -Every option and exit code: [create-clip-wallet CLI](../reference/cli.md). +Every icon is rendered from the logo with Node alone: PNG decoding and encoding, area-average resampling with +premultiplied alpha, and the ICO and ICNS containers are written by create-clip-wallet itself. A PNG logo needs nothing +else. An SVG logo is rasterised once at 1024 px by the first of these that is installed: `@resvg/resvg-js` (in the +project), `rsvg-convert` (librsvg), `sips` (built into macOS) or ImageMagick. Without any of them, pass a +1024×1024 PNG. + +| File | For | +| --- | --- | +| `packages/extension/public/icon/{16,32,48,128}.png` | the extension manifest | +| `packages/desktop/build/icon.icns` | macOS (16–1024 px with Retina pairs; artwork on Apple's 824/1024 grid) | +| `packages/desktop/build/icon.ico` | Windows (16, 24, 32, 48, 64, 128, 256) | +| `packages/desktop/build/icons/x.png`, `build/icon.png` | Linux (16–1024), window icon | +| `packages/desktop/src/renderer/public/tray/tray*.png`, `trayTemplate*.png` | tray, and the macOS menu-bar template (black silhouette) | +| `packages/mobile/assets/icon.png` | iOS / Expo icon: 1024 px, opaque (a transparent logo is set on the accent colour) | +| `packages/mobile/assets/adaptive-icon.png`, `adaptive-monochrome.png` | Android adaptive icon (logo in the 66% safe zone, accent background) and the Android 13 themed icon | +| `packages/mobile/assets/splash-icon.png` | splash screen (on the accent colour) | + +A later `--accent` change re-renders with the same logo; only the starter mark is redrawn in the new colour. + +## Commands in a project + +| Command | | +| --- | --- | +| `pnpm wallet:identity` | Set or change the identity (`--name --rdns --id --accent --logo --homepage --description --walletconnect-project-id --new-key`). Keeps the extension key (and id) unless `--new-key`. | +| `pnpm wallet:brand` | Render every platform's icons again from the logo (`--logo file`, `--accent #hex`). | +| `pnpm wallet:listings` | Regenerate `docs/listings/` after an identity change. | +| `pnpm wallet:mainnet-check` | What still blocks a mainnet build: exit `0` ready, `2` work left, `1` mainnet on but not ready. | + +Each runs the project's pinned `create-clip-wallet` (`create-clip-wallet identity`, `brand`, `listings`, +`mainnet-check`). The build commands for each platform are on [Build and ship](./build-and-ship.md). ## Same project as the template -`npx create-clip-wallet my-wallet` and `npm create scaffold-hbar@latest -- --template ColdAI-org/scaffold-hbar-clip-wallet` followed by -`pnpm wallet:identity` give the same files: both skip `.git`, `node_modules` and `.env`, apply the template's rename -map, `git init`, then run the same identity code. The kit's CI checks it (`pnpm kit:e2e` and -`pnpm kit:e2e:scaffold-hbar`, from packed tarballs). +`pnpm wallet:identity` gives exactly what `npx create-clip-wallet --scaffold-hbar` gives (every platform plus +the dapp): both skip `.git`, `node_modules` and `.env`, apply the template's rename map, `git init`, then run the same +identity code. With fewer `--platforms`, or without `--scaffold-hbar`, create-clip-wallet then removes those parts: +their folders, their root scripts, and their sections of `README.md`, `AGENTS.md`, `llms.txt`, `MAINNET.md`, +`docs/signing.md` and the CI workflow (marked `platform:` in the template). + +## How the kit tests it + +`pnpm kit:e2e` creates a wallet with every platform from the packed tarballs and checks the identity and icons; +install, harness and types; the extension build, manifest, extension id and EIP-6963 announcement in Chromium; the +desktop build, electron-builder for the host (on macOS arm64, the `.app` and zip) with bundle id, name, scheme and icon +checked in `Info.plist`, and the packaged app started headless to onboarding; the phone app's Expo config, +`expo export` for iOS and Android (with the wallet config inside the bundle) and `expo prebuild` (bundle id, package, +name, scheme and icons in the generated projects); the mainnet gate on every platform; and the security floor. +`--dapp` adds a Scaffold-HBAR project and its Next.js build; `--skip-desktop` and `--skip-mobile` are for machines +without those toolchains. `pnpm kit:e2e:scaffold-hbar` checks the Scaffold-HBAR route gives the same project. diff --git a/apps/docs/src/kit/index.md b/apps/docs/src/kit/index.md index 063c4f2..4165644 100644 --- a/apps/docs/src/kit/index.md +++ b/apps/docs/src/kit/index.md @@ -1,21 +1,35 @@ # Launch your own wallet The Clip Wallet kit lets you ship a wallet under your own name on the same code Clip Wallet runs: fourteen network -families, 1Mask for every dapp, decoded approvals, the security floor, features, plugins and route-and-fund. You change -identity and configuration; the wallet itself is versioned, signed npm packages. +families, 1Mask for every dapp, decoded approvals, the security floor, features, plugins and route-and-fund. One +project gives you the wallet **on every platform**: a browser extension, a desktop app for macOS, Windows and Linux, +and a phone app for iOS and Android, all described by one `clip.config.ts`. You change identity and configuration; the +wallet itself is versioned, signed npm packages. ::: warning Test networks first -A new wallet runs on test networks. Mainnet is a later decision by the wallet's owner, behind a checklist the build and -the harness both enforce. See [Build and ship](./build-and-ship.md#mainnet). +A new wallet runs on test networks. Mainnet is a later decision by the wallet's owner, behind a checklist the builds and +the harness all enforce. See [Build and ship](./build-and-ship.md#mainnet). ::: ## What you get +| Platform | Folder | Built with | On the kit | +| --- | --- | --- | --- | +| Browser extension (Chrome, Edge, Brave, Firefox) | `packages/extension` | WXT | `@clip-wallet/extension-kit` | +| Desktop app (macOS, Windows, Linux) | `packages/desktop` | Electron, electron-vite, electron-builder | `@clip-wallet/desktop-kit` | +| Phone app (iOS, Android) | `packages/mobile` | Expo (React Native) | `@clip-wallet/mobile-kit` | +| Scaffold-HBAR demo dapp (optional) | `packages/nextjs` | Next.js | `@clip-wallet/connect` | + +Each platform folder holds one-line entrypoints, a build config of one call and the icons. The wallet itself (vault, +engine, approvals, screens, 1Mask, the security floor) comes from the kit packages, pinned to one signed release. +Nothing in a project imports monorepo internals. + | | | | --- | --- | -| **A browser extension with its own identity** | Its own name, icon, extension id, EIP-6963 rdns and WalletConnect project. It never announces itself as Clip Wallet. | -| **Every dapp works** | EIP-1193 + EIP-6963, the Wallet Standard, AIP-62, CIP-30, `injectedWeb3`, get-starknet, TON Connect, NEAR Connect, SEP-43, Beacon and WalletConnect, all under your identity. | +| **Its own identity everywhere** | Its own name, icon, extension id, EIP-6963 rdns, app ids, deep-link scheme and WalletConnect project. It never announces itself as Clip Wallet. | +| **Every dapp works** | EIP-1193 + EIP-6963, the Wallet Standard, AIP-62, CIP-30, `injectedWeb3`, get-starknet, TON Connect, NEAR Connect, SEP-43, Beacon and WalletConnect, all under your identity, in the extension and in the desktop and phone apps' built-in browsers. | | **A security floor you can't switch off** | Open phishing lists, look-alike and new-contract checks, decode-before-approve, approval review and revoke. | +| **Icons from one logo** | Every platform's icons (extension, `.icns`, `.ico`, Linux, tray, iOS, Android adaptive, splash) rendered from your logo with Node alone. | | **A Scaffold-HBAR dapp** | Next.js on Hedera testnet that connects to your wallet, signs, sends, and demos Clip Connect. | | **Listing drafts** | For TON Connect, NEAR, Stellar Wallets Kit, Beacon, use-wallet, WalletConnect Explorer and EIP-6963, filled in with your identity. | | **Agent tooling** | `AGENTS.md`, `llms.txt`, `.harness/` and `pnpm harness` in every project. See [Work with AI agents](./ai-agents.md). | @@ -33,22 +47,38 @@ npm create scaffold-hbar@latest -- --template ColdAI-org/scaffold-hbar-clip-wall cd && pnpm install && pnpm wallet:identity --name "Acme Wallet" --rdns com.acme.wallet ``` -Both produce **the same project**: they copy the same template the same way, then run the same identity step. +Both produce **the same project**: they copy the same template the same way, then run the same identity step. The +Scaffold-HBAR route gives every platform plus the dapp, which is what `npx create-clip-wallet --scaffold-hbar` gives. ## The steps -1. [Create the project](./create-clip-wallet.md) and give it an identity. -2. [Configure it](./config.md): accent, networks, routing, hardware, services. -3. [Build, test and ship it](./build-and-ship.md): load it unpacked, package it for the stores, submit listings. -4. Keep it current: upgrade the pinned kit version deliberately, read the changelog, verify provenance. +1. [Create the project](./create-clip-wallet.md) and give it an identity and a logo. +2. [Configure it](./config.md): accent, networks, languages, app ids, routing, hardware, services. +3. [Build, test and ship it](./build-and-ship.md): the extension, the desktop app and the phone app. +4. [Sign it and open store accounts](./signing.md) when you are ready to ship. Nothing before that needs an account. +5. Keep it current: upgrade the pinned kit version deliberately, read the changelog, verify provenance. ## What lives where in your project ```text -packages/extension/ your wallet: wallet.identity.json, clip.config.ts, icon, one-line WXT entrypoints -packages/nextjs/ the Scaffold-HBAR dapp: home page, /clip-connect, /debug, Hedera testnet -docs/listings/ listing drafts for your identity -tools/harness/check.mjs the rules (pnpm harness) -tools/verify-provenance.mjs checks the kit packages were built by the kit's CI -.harness/ AGENTS.md llms.txt how agents (and people) change the project safely +clip.config.ts the wallet, for every platform (typed by @clip-wallet/config) +wallet.identity.json name, description, rdns, homepage, icon, appId, extension.key (written by wallet:identity) +icon.svg | icon.png the logo; every platform's icons are rendered from it +MAINNET.md the owner's mainnet checklist +.env CLIP_* build values (WalletConnect id, partner keys); never committed +.keys/extension.pem the extension's private key; never committed +packages/extension/ wxt.config.ts = clipWallet({ config, configDir: "../.." }); one-line entrypoints; public/icon/ +packages/desktop/ electron.vite.config.ts = clipDesktop(…); electron-builder.config.cjs = electronBuilderConfig(…); + one-line main / preload / renderer entrypoints; build/ icons and entitlements; tray icons +packages/mobile/ app.config.ts = expoConfig(…); metro.config.js = withClipWallet(…); index.ts = registerClipWallet(); + assets/ icons and splash; eas.json +packages/nextjs/ the Scaffold-HBAR dapp (with --scaffold-hbar): home page, /clip-connect, /debug +docs/listings/ listing-submission drafts for your identity +docs/signing.md code signing and store accounts: what each needs, and the variable names (never values) +tools/harness/check.mjs the rules, as checks (pnpm harness) +tools/verify-provenance.mjs checks the kit packages were built by the kit's CI +.harness/ AGENTS.md llms.txt how agents (and people) change the project safely ``` + +Every `@clip-wallet/*` package (and `create-clip-wallet`) is pinned to one exact version in the root and platform +`package.json` files; `pnpm verify:provenance` checks each one's npm provenance. diff --git a/apps/docs/src/kit/scaffold-hbar.md b/apps/docs/src/kit/scaffold-hbar.md index d5266ae..55dd36b 100644 --- a/apps/docs/src/kit/scaffold-hbar.md +++ b/apps/docs/src/kit/scaffold-hbar.md @@ -1,8 +1,9 @@ # Scaffold-HBAR template The template (`templates/scaffold-hbar-clip-wallet` in the repo, published as `ColdAI-org/scaffold-hbar-clip-wallet`) -is a [Scaffold-HBAR](https://github.com/hedera-dev/scaffold-hbar) project with two packages: **your wallet** as a -browser extension, and a **Next.js dapp** on Hedera testnet that connects to it. +is a [Scaffold-HBAR](https://github.com/hedera-dev/scaffold-hbar) project with **your wallet** on every platform (a +browser extension, a desktop app and a phone app, from one `clip.config.ts` at the root) and a **Next.js dapp** on +Hedera testnet that connects to it. It is the same project as `npx create-clip-wallet --scaffold-hbar`. ```sh npm create scaffold-hbar@latest -- --template ColdAI-org/scaffold-hbar-clip-wallet @@ -13,7 +14,7 @@ pnpm extension:build # → packages/extension/.output/chrome-mv3 pnpm next:dev # → http://localhost:3000 ``` -Prerequisites: Node 22 or later, pnpm 10 or later, Git, and a Chromium browser. Load the extension unpacked, create a +Prerequisites: Node 22.18 or later, pnpm 10 or later, Git, and a Chromium browser. Load the extension unpacked, create a wallet in it, get testnet HBAR from the [Hedera portal faucet](https://portal.hedera.com/faucet), and open the dapp. ## The demo dapp @@ -33,10 +34,12 @@ your wallet's approval window, decoded in plain words. | --- | --- | | `pnpm extension:build` / `extension:dev` / `extension:zip` | build, watch, or zip the extension for the stores | | `pnpm extension:build:fixtures` | the extension with sample data and no network, for screenshots | +| `pnpm dev:desktop` / `desktop:build` / `desktop:dist` | run, build, or package the desktop app (installers in `packages/desktop/release`) | +| `pnpm mobile:start` / `mobile:prebuild` / `mobile:export` | the phone app: Metro for a development build, the native projects, the JavaScript bundles | | `pnpm next:dev` / `next:build` / `next:serve` | the demo dapp | | `pnpm harness` | the rules; must pass before every commit | -| `pnpm check-types`, `pnpm build`, `pnpm lint` | types, both builds, lint | -| `pnpm wallet:identity`, `wallet:listings`, `wallet:mainnet-check` | identity, listing drafts, what blocks mainnet | +| `pnpm check-types`, `pnpm build`, `pnpm lint` | types, the builds, lint | +| `pnpm wallet:identity`, `wallet:brand`, `wallet:listings`, `wallet:mainnet-check` | identity, every platform's icons from the logo, listing drafts, what blocks mainnet | | `pnpm verify:provenance` | checks the kit packages were built by the kit's CI from its public repository | ## How it relates to the monorepo diff --git a/apps/docs/src/kit/signing.md b/apps/docs/src/kit/signing.md new file mode 100644 index 0000000..8a46801 --- /dev/null +++ b/apps/docs/src/kit/signing.md @@ -0,0 +1,91 @@ +# Stores and code signing + +No account or certificate is needed to **build** a kit-built wallet: the extension zip, the desktop installers +(unsigned) and the phone app's JavaScript bundles and native projects all build on a fresh checkout. **Shipping** to +people needs the accounts and certificates below. Every project has its own copy of this list in `docs/signing.md`, +trimmed to the platforms it has. + +::: warning Secrets stay out of the repository +Put the values in your CI's secret store (or a local `.env` that is never committed): never in the repository, never in +`clip.config.ts`. The kit reads only the **names** below; nothing in the project holds a value. +::: + +## What each platform needs + +| Platform | Account | Certificate or key | Variables (CI secrets) | +| --- | --- | --- | --- | +| Chrome Web Store | Google developer account (one-time fee) | the extension key in `.keys/extension.pem` (fixes the extension id) | `CHROME_EXTENSION_ID`, `CHROME_CLIENT_ID`, `CHROME_CLIENT_SECRET`, `CHROME_REFRESH_TOKEN` (`wxt submit`) | +| Firefox Add-ons | addons.mozilla.org account | AMO API key and secret | `FIREFOX_EXTENSION_ID`, `FIREFOX_JWT_ISSUER`, `FIREFOX_JWT_SECRET` (`wxt submit`) | +| Microsoft Edge Add-ons | Partner Center account | Edge API credentials | `EDGE_PRODUCT_ID`, `EDGE_CLIENT_ID`, `EDGE_API_KEY` (`wxt submit`) | +| macOS | Apple Developer Program | Developer ID Application certificate; notarization API key | `CSC_LINK`, `CSC_KEY_PASSWORD` (or `CSC_NAME`); `APPLE_API_KEY`, `APPLE_API_KEY_ID`, `APPLE_API_ISSUER` (or `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`) | +| Windows | an OV or EV code-signing certificate, or Azure Trusted Signing | the certificate (`.pfx`) or Azure credentials | `WIN_CSC_LINK`, `WIN_CSC_KEY_PASSWORD` (Azure: `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`) | +| Linux | none (signing optional) | an optional GPG key for your own apt repository | your packaging | +| Desktop auto-updates | a GitHub repository for releases | | `CLIP_UPDATES=1`, `CLIP_UPDATES_OWNER`, `CLIP_UPDATES_REPO`, `GH_TOKEN` | +| iOS | Apple Developer Program, App Store Connect | distribution certificate and provisioning profile (EAS can manage them) | `EXPO_TOKEN` (EAS); `EXPO_ASC_API_KEY_PATH`, `EXPO_ASC_API_KEY_ID`, `EXPO_ASC_API_KEY_ISSUER_ID`, `EXPO_APPLE_TEAM_ID` (`eas submit`) | +| Android | Google Play Console | upload keystore (EAS can keep it); a Play service-account key for uploads | `EXPO_TOKEN`; the service-account key file path in `eas.json`, written from a secret | +| Universal links and passkeys | a domain you control | the domain's `apple-app-site-association` and `assetlinks.json` | `CLIP_ASSOCIATED_DOMAIN` | + +## Browser extension + +```sh +pnpm extension:zip # packages/extension/.output/*.zip for the stores +``` + +Keep `.keys/extension.pem` backed up offline: the Chrome Web Store item must use the same key, or the extension id (and +every listing that names it, in `docs/listings/`) changes. `FIREFOX_EXTENSION_ID` is your gecko id +(`wallet@`). + +## Desktop app + +```sh +pnpm desktop:dist # installers for this computer's OS → packages/desktop/release/ +pnpm desktop:dist:mac # dmg + zip, arm64 and x64 +pnpm desktop:dist:win # NSIS installer + zip (NSIS needs Windows or CI) +pnpm desktop:dist:linux # AppImage + deb (Linux or CI) + tar.gz (anywhere) +``` + +Without the signing variables the builds are unsigned: fine for testing, not for people (macOS Gatekeeper blocks +unsigned apps; Windows SmartScreen warns). `electronBuilderConfig()` turns signing, notarization and update publishing +on only when their variables are set. + +- **macOS signing**: the Developer ID Application certificate exported as `.p12`, in `CSC_LINK` (a file path or base64) + with `CSC_KEY_PASSWORD`, or `CSC_NAME` for an identity already in the build machine's keychain. +- **macOS notarization**: an App Store Connect API key (`APPLE_API_KEY`, the path to the `.p8`, with + `APPLE_API_KEY_ID` and `APPLE_API_ISSUER`), or an Apple ID (`APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, + `APPLE_TEAM_ID`). +- **Windows signing**: `WIN_CSC_LINK` (a `.pfx` path or base64) and `WIN_CSC_KEY_PASSWORD`. EV certificates on a + hardware token, or Azure Trusted Signing, need electron-builder's `win.signtoolOptions` or `win.azureSignOptions`, + added over `electronBuilderConfig()` in `packages/desktop/electron-builder.config.cjs`. +- **Auto-updates** are off by default and for signed builds only: `CLIP_UPDATES=1` with `CLIP_UPDATES_OWNER` and + `CLIP_UPDATES_REPO` at build time, and `GH_TOKEN` to publish. +- **Your extension talking to the desktop app** (native messaging): `CLIP_EXTENSION_IDS` lists your extension's + Chromium ids at build time. + +The app id (`.desktop`), product name and deep-link scheme come from `clip.config.ts` and +`wallet.identity.json` (see [Platforms](./config.md#platforms)). + +## Phone app + +```sh +pnpm mobile:export # JavaScript bundles for iOS and Android (no account) +pnpm mobile:prebuild # packages/mobile/ios and android (generated, never committed) +pnpm --filter mobile eas:build # cloud builds with EAS (needs EXPO_TOKEN) +``` + +- **Expo / EAS** is optional: you can build `ios/` in Xcode and `android/` in Android Studio instead. With EAS, an Expo + account and `EXPO_TOKEN` in CI; `packages/mobile/eas.json` has development, preview and production profiles. +- **iOS**: an Apple Developer Program membership, the bundle id (`mobile.bundleId` or `appId`) registered in your + developer account, and a distribution certificate and provisioning profile (EAS creates and stores them when you let + it). App Store uploads (`eas submit`) read an App Store Connect API key. +- **Android**: a Google Play Console account and an upload keystore (EAS can generate and keep it; if you build with + Gradle yourself, keep the keystore and its passwords out of git and pass them from your CI's secrets). Play uploads + use a Google Cloud service-account JSON key: point `submit.production.android.serviceAccountKeyPath` in `eas.json` at + a file your CI writes from a secret. +- **Universal links and passkeys**: `CLIP_ASSOCIATED_DOMAIN` at build time, plus the domain's + `/.well-known/apple-app-site-association` and `/.well-known/assetlinks.json`. They need signed builds. + +## Before you submit + +Store listings also need a privacy policy, a support contact and screenshots. The stores' data-safety forms must match +what Settings → Security says the wallet sends where, and the listing drafts in `docs/listings/` (`pnpm wallet:listings`) +must name your own identity. Mainnet stays a separate decision: see [Mainnet](./build-and-ship.md#mainnet). diff --git a/apps/docs/src/snippets/kit/clip.config.ts b/apps/docs/src/snippets/kit/project/clip.config.ts similarity index 87% rename from apps/docs/src/snippets/kit/clip.config.ts rename to apps/docs/src/snippets/kit/project/clip.config.ts index f27600f..5892433 100644 --- a/apps/docs/src/snippets/kit/clip.config.ts +++ b/apps/docs/src/snippets/kit/project/clip.config.ts @@ -1,4 +1,4 @@ -// packages/extension/clip.config.ts in a kit-built wallet (apps/extension/clip.config.ts in this repo). +// clip.config.ts at the root of a kit-built wallet, for every platform (apps/*/clip.config.ts in this repo). import { defineConfig } from "@clip-wallet/config"; export default defineConfig({ diff --git a/apps/docs/src/snippets/kit/project/packages/desktop/electron.vite.config.ts b/apps/docs/src/snippets/kit/project/packages/desktop/electron.vite.config.ts new file mode 100644 index 0000000..49e3e10 --- /dev/null +++ b/apps/docs/src/snippets/kit/project/packages/desktop/electron.vite.config.ts @@ -0,0 +1,6 @@ +// packages/desktop/electron.vite.config.ts: the whole desktop app comes from @clip-wallet/desktop-kit. +// clipDesktop() returns the complete electron-vite config (main process, preloads, pages, native-messaging host). +import { clipDesktop } from "@clip-wallet/desktop-kit/electron-vite"; +import clipConfig from "../../clip.config"; + +export default clipDesktop({ config: clipConfig, root: import.meta.dirname, configDir: "../.." }); diff --git a/apps/docs/src/snippets/kit/wxt.config.ts b/apps/docs/src/snippets/kit/project/packages/extension/wxt.config.ts similarity index 56% rename from apps/docs/src/snippets/kit/wxt.config.ts rename to apps/docs/src/snippets/kit/project/packages/extension/wxt.config.ts index af02e5a..52cede8 100644 --- a/apps/docs/src/snippets/kit/wxt.config.ts +++ b/apps/docs/src/snippets/kit/project/packages/extension/wxt.config.ts @@ -1,6 +1,7 @@ // packages/extension/wxt.config.ts: the whole extension comes from @clip-wallet/extension-kit. +// configDir: the project's one clip.config.ts, icon, MAINNET.md and .env live two folders up, at the root. import { defineConfig } from "wxt"; import { clipWallet } from "@clip-wallet/extension-kit/wxt"; -import clipConfig from "./clip.config"; +import clipConfig from "../../clip.config"; -export default defineConfig(clipWallet({ config: clipConfig, root: import.meta.dirname })); +export default defineConfig(clipWallet({ config: clipConfig, root: import.meta.dirname, configDir: "../.." })); diff --git a/apps/docs/src/snippets/kit/project/packages/mobile/app.config.ts b/apps/docs/src/snippets/kit/project/packages/mobile/app.config.ts new file mode 100644 index 0000000..fcbf681 --- /dev/null +++ b/apps/docs/src/snippets/kit/project/packages/mobile/app.config.ts @@ -0,0 +1,4 @@ +// packages/mobile/app.config.ts: Expo's app config from the project's clip.config.ts (@clip-wallet/mobile-kit). +import { expoConfig } from "@clip-wallet/mobile-kit/expo"; + +export default () => expoConfig({ configFile: "../../clip.config.ts", root: __dirname }); diff --git a/apps/docs/test/snippets.test.ts b/apps/docs/test/snippets.test.ts index 830088f..6ac4ef2 100644 --- a/apps/docs/test/snippets.test.ts +++ b/apps/docs/test/snippets.test.ts @@ -143,7 +143,18 @@ describe("pages", () => { manifests.set(p.name.replace(/^@clip-wallet\//, ""), p.scripts ?? {}); } } - const rootScripts = (JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { scripts: Record }).scripts; + // A kit-built wallet's packages (packages/extension, desktop, mobile, nextjs as @sh/*): the pages show its commands too. + const template = join(root, "templates", "scaffold-hbar-clip-wallet"); + for (const d of readdirSync(join(template, "packages"))) { + const pj = join(template, "packages", d, "package.json"); + if (!existsSync(pj)) continue; + const p = JSON.parse(readFileSync(pj, "utf8")) as { name: string; scripts?: Record }; + const short = p.name.replace(/^@sh\//, ""); + manifests.set(p.name, p.scripts ?? {}); + manifests.set(short, { ...manifests.get(short), ...p.scripts }); + } + const readScripts = (dir: string) => (JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as { scripts: Record }).scripts; + const rootScripts = { ...readScripts(template), ...readScripts(root) }; const bad: string[] = []; let seen = 0; for (const f of all) { @@ -223,8 +234,8 @@ async function run(path: string): Promise { describe("runnable snippets print what the docs say", () => { afterEach(() => vi.restoreAllMocks()); - it("kit/clip.config.ts is a valid configuration", async () => { - const mod = (await import(pathToFileURL(join(snippets, "kit/clip.config.ts")).href)) as { default: { name: string; mainnet: unknown; networks: string[] } }; + it("kit/project/clip.config.ts is a valid configuration", async () => { + const mod = (await import(pathToFileURL(join(snippets, "kit/project/clip.config.ts")).href)) as { default: { name: string; mainnet: unknown; networks: string[] } }; expect(mod.default.name).toBe("Acme Wallet"); expect(mod.default.mainnet).toBe(false); }); diff --git a/apps/mobile/README.md b/apps/mobile/README.md index 01135df..3200339 100644 --- a/apps/mobile/README.md +++ b/apps/mobile/README.md @@ -1,5 +1,11 @@ # Clip Wallet mobile (iOS + Android) +> The app's code is [`@clip-wallet/mobile-kit`](../../packages/mobile-kit) (`packages/mobile-kit`), shared with every +> kit-built wallet; this folder is Clip Wallet's brand on it: `clip.config.ts`, `assets/` rendered from `brand/`, +> `index.ts` (`registerClipWallet()`), `app.config.ts` (`expoConfig()`), `metro.config.js` (`withClipWallet()`) and the +> local test dapp. Paths in "What's where" (`src/…`, `test/…`) are in the kit; its tests run with +> `pnpm --filter @clip-wallet/mobile-kit test`. + Expo SDK 57 (React Native 0.86.3, React 19.2.3, Hermes, New Architecture, which is the only architecture in RN 0.82+). It runs the same packages as the extension through `@clip-wallet/engine`. Testnets only. @@ -12,7 +18,7 @@ cp .env.example .env # optional: EXPO_PUBLIC_WC_PROJECT_ID, EXPO_PU pnpm ios # builds the inpage bundle, then `expo run:ios` (needs Xcode, CocoaPods, an iOS Simulator runtime) pnpm android # `expo run:android` pnpm dapp # local test dapp on http://localhost:8787 (EIP-6963 + personal_sign); open it in Browse -pnpm test # vitest (bridge e2e, Argon2, deep links, storage) + jest-expo (screens) +pnpm --filter @clip-wallet/mobile-kit test # vitest (bridge e2e, Argon2, deep links, storage) + jest-expo (screens) pnpm typecheck pnpm export:ios # Metro + Hermes bytecode bundle only (no Xcode needed) ``` diff --git a/llms.txt b/llms.txt index 119b7ea..7d636fe 100644 --- a/llms.txt +++ b/llms.txt @@ -1,6 +1,6 @@ # Clip Wallet -> Non-custodial multi-chain wallet for every CLPR network (14 families), and a kit for building your own: @clip-wallet/* packages, @clip-wallet/extension-kit, create-clip-wallet and a Scaffold-HBAR template. Pre-release: test networks only. pnpm monorepo, TypeScript, Apache-2.0. +> Non-custodial multi-chain wallet for every CLPR network (14 families), and a kit for building your own on every platform: @clip-wallet/* packages, @clip-wallet/extension-kit, desktop-kit and mobile-kit, create-clip-wallet and a Scaffold-HBAR template. Pre-release: test networks only. pnpm monorepo, TypeScript, Apache-2.0. Read first: AGENTS.md (rules + recipes), .harness/spec.md (product), .harness/prd.md (user stories), packages/core/src/index.ts (the contract), packages/config/src/index.ts (clip.config.ts schema). Developer docs: https://coldai.org/clip/docs/ (sources: apps/docs/src, plain Markdown; every code sample is a typechecked file in apps/docs/src/snippets). Index of the pages agents need most: "## Docs" below. @@ -15,13 +15,13 @@ Developer docs: https://coldai.org/clip/docs/ (sources: apps/docs/src, plain Mar ## Rules (enforced by pnpm harness) - Only packages/vault touches phrases, seeds, private keys, signing and Argon2. -- Only packages/vault, the extension background (packages/extension-kit/src/background, apps/extension background), the mobile background and the onboarding screen import @clip-wallet/vault. +- Only packages/vault, the extension background (packages/extension-kit/src/background, apps/extension background), the phone app's background (packages/mobile-kit/src/background), the desktop main-process host (packages/desktop-kit/src/main/host) and the onboarding screen import @clip-wallet/vault. - Chain modules (packages/chains-*) implement ChainModule and never import or depend on the vault. - Never log or commit phrases, keys, secrets, .env values or key files (*.pem, .keys/). Tests use public BIP-39 vectors only. - Every dapp request becomes a DecodedRequest before approval; blind signing is off by default. - Networks are invisible in the default UI; testnet by default; mainnet needs the clip.config.ts checklist and mainnetProblems() empty. - The security floor (open phishing lists, decode-before-approve) has no switch; securityFloorProblems() refuses mainnet configs below it. -- Kit-built wallets (packages/extension/clip.config.ts, no packages/vault): own identity (never org.coldai.* / "Clip Wallet"), clipWallet() build, MAINNET.md boxes before mainnet, kit packages pinned exactly. +- Kit-built wallets (clip.config.ts + wallet.identity.json at the root, no packages/vault): own identity (never org.coldai.* / "Clip Wallet"), every platform built through its kit (clipWallet, clipDesktop + electronBuilderConfig, expoConfig + withClipWallet), MAINNET.md boxes before mainnet, kit packages pinned exactly. - Published packages: runtime imports declared as dependencies/peers; manifests in the published shape (tools/release/check-manifests.mjs); template in sync (tools/release/sync-template.mjs --check). - User-facing errors are ClipError(userMessage in plain words, code). @@ -39,7 +39,10 @@ Developer docs: https://coldai.org/clip/docs/ (sources: apps/docs/src, plain Mar - chains-evm, -hedera, -solana, -bitcoin, -sui, -aptos, -cardano, -substrate, -starknet, -ton, -near, -stellar, -tezos, -algorand: ChainModule per family. - 1mask: connectors (EIP-1193/6963, Wallet Standard, AIP-62, CIP-30, injectedWeb3, get-starknet, TON Connect, NEAR/Stellar/Algorand globals, Beacon relay, WalletConnect v2) announcing the wallet's identity. - engine: approvals, permissions, portfolio, catalog (engine/catalog), wiring shared by extension and mobile. -- extension-kit: the MV3 extension as a library. /wxt clipWallet({config}) (Node), /background, /content, /inpage, /pages, /plugin-host, /plugin-sandbox. Security floor and mainnet checklist enforced at build. +- extension-kit: the MV3 extension as a library. /wxt clipWallet({config, configDir}) (Node), /background, /content, /inpage, /pages, /plugin-host, /plugin-sandbox. Security floor and mainnet checklist enforced at build. +- desktop-kit: the Electron app (macOS, Windows, Linux) as a library. /electron-vite clipDesktop({config}) and /builder electronBuilderConfig() (Node), /main, /preload/wallet, /preload/dapp, /renderer (mountWallet, mountApproval, mountBrowserChrome), /native-host. Config as virtual:clip-wallet/config. +- mobile-kit: the Expo app (iOS, Android) as a library. /expo expoConfig() and /metro withClipWallet() (Node), /polyfills, registerClipWallet(). Native modules are peers. +- config: clip.config.ts schema (identity, theme, networks, languages, appId, scheme, desktop/mobile ids, services, fees/usage reserved for Clip Cloud, mainnet), platformIds(); /node loadClipConfigSync() and build-environment helpers. - ui: React screens and theme tokens. i18n: English + 11 languages. - route: findShortfall, RouteClient.quote / planPayOnHedera / trackRoute; settleOnHedera (Phase 3, SETTLE_DEPLOYMENTS). Planner vendored from ColdAI-org/clprouter. - security: threat intel (open lists, heuristics, optional Blockaid), approvals review/revoke, spam cleanup, securityFloorProblems. @@ -51,13 +54,14 @@ Developer docs: https://coldai.org/clip/docs/ (sources: apps/docs/src, plain Mar - connect: Clip Connect, the dapp-side SDK (EIP-6963/Wallet Standard discovery preferring Clip, CAIP-10 accounts, pay() with EIP-5792 + ERC-7682 auxiliary funds, React/wagmi/Solana adapters). Compatibility promise and suite: docs/compat.md. - kit-modules: NEAR Wallet Selector, Stellar Wallets Kit, use-wallet, Beacon peer. - backup-client, media-client: clients for services/backup and services/media-proxy. -- create-clip-wallet: npx create-clip-wallet (template + identity + listing drafts); identity | listings | mainnet-check. +- create-clip-wallet: npx create-clip-wallet [--platforms extension,desktop,mobile --scaffold-hbar --id --logo --languages …] (template + parts + identity + every platform's icons from one logo + listing drafts); identity | brand | listings | mainnet-check. ## Apps, services, template - apps/extension: Clip Wallet's own identity on extension-kit; Playwright e2e; store packages (pnpm --filter @clip-wallet/extension package, scripts/repro-check.sh). brand/: the Clip Wallet mark (tools/brand/render.mjs). -- apps/mobile: Expo app on engine. +- apps/desktop: Clip Wallet's own identity on desktop-kit; Playwright e2e (real app); electron-builder packaging. +- apps/mobile: Clip Wallet's own identity on mobile-kit (Expo). - services/backup, services/media-proxy, services/link-relay: Cloudflare Workers. contracts/handles: ClipHandles (Foundry). -- templates/scaffold-hbar-clip-wallet: the Scaffold-HBAR template (ColdAI-org/scaffold-hbar-clip-wallet): packages/extension (wallet) + packages/nextjs (dapp, /debug), AGENTS.md, .harness, llms.txt, CI and fresh-scaffold workflows. +- templates/scaffold-hbar-clip-wallet: the Scaffold-HBAR template (ColdAI-org/scaffold-hbar-clip-wallet) and what create-clip-wallet copies: clip.config.ts + wallet.identity.json at the root, packages/extension, packages/desktop, packages/mobile, packages/nextjs (dapp, /debug), docs/signing.md, AGENTS.md, .harness, llms.txt, CI and fresh-scaffold workflows. Parts are marked platform: in docs. - tools/harness/check.mjs: the rules above as checks (file:line, plain words). tools/release: build, manifests, pack-all, template sync. tools/kit: e2e. ## Docs (https://coldai.org/clip/docs/; Markdown sources in apps/docs/src/.md) @@ -66,7 +70,7 @@ Developer docs: https://coldai.org/clip/docs/ (sources: apps/docs/src, plain Mar - architecture/{vault,engine,extension-kit,onemask,chain-modules,route-settle,features,security,link,plugins,social-names-hardware,services}: one page per part. - dapps/: per-ecosystem guides (EIP-1193/6963 with wagmi, RainbowKit, AppKit; Wallet Standard for Solana, Sui, Aptos, Bitcoin; CIP-30; injectWeb3; Starknet; TON Connect; NEAR, Stellar, Algorand kit modules; Tezos Beacon; Hedera; WalletConnect), each linked to the dapp and picker matrix results. - connect/: Clip Connect (@clip-wallet/connect): connect(), request(), pay(), canPay(), balances(), React hooks, wagmi connector, Solana adapter, EIP-5792 wallet calls, ERC-7682 auxiliary funds, the compatibility promise. -- kit/: create-clip-wallet, the Scaffold-HBAR template, clip.config.ts (networks, mainnet opt-in, branding, partner keys, services), build and store packaging, working with agents. +- kit/: one project for every platform (extension, desktop, phone), create-clip-wallet (options, icons from one logo), the Scaffold-HBAR template, clip.config.ts (platform ids, languages, networks, mainnet opt-in, branding, partner keys, services), building each platform, stores and code signing (kit/signing), working with agents. - extend/chain-module (a complete worked ChainModule), extend/networks-and-tokens, extend/plugins (SES sandbox, manifest, permissions, insights API), extend/languages (ICU, glossary, bidi isolates), extend/threat-provider. - services/: backup, media-proxy, link-relay: APIs, self-hosting on Cloudflare with wrangler, secret names. - security/: threat model, vault cryptography, approval-bound signing, sanitization, scam checks, hardware wallets, disclosure. @@ -77,6 +81,6 @@ Developer docs: https://coldai.org/clip/docs/ (sources: apps/docs/src, plain Mar ## Optional - .harness/README.md: every harness rule and what to do when one is wrong - .changeset/README.md: versioning and the CI-only release -- packages/create-clip-wallet/README.md, packages/extension-kit/README.md +- packages/create-clip-wallet/README.md, packages/extension-kit/README.md, packages/desktop-kit/README.md, packages/mobile-kit/README.md - templates/scaffold-hbar-clip-wallet/README.md - https://github.com/ColdAI-org/clprouter: CLPRouter contracts, SDK, services, deployments diff --git a/packages/config/README.md b/packages/config/README.md index c6d9c38..8a95d9f 100644 --- a/packages/config/README.md +++ b/packages/config/README.md @@ -1,7 +1,8 @@ # @clip-wallet/config -The typed schema for `clip.config.ts`, the one file a wallet maker edits: identity (name, description, rdns, homepage, -icon, extension key), theme, networks (14 families), routing defaults and settle-on-Hedera, hardware wallets, +The typed schema for `clip.config.ts`, the one file a wallet maker edits, for every platform the wallet ships (browser +extension, desktop app, phone app): identity (name, description, rdns, homepage, icon, extension key, app ids, +deep-link scheme), theme, networks (14 families), languages, routing defaults and settle-on-Hedera, hardware wallets, WalletConnect, passkeys, optional hosted services, and the mainnet switch. `defineConfig()` validates and fills defaults; problems come back as plain sentences, one per setting @@ -36,6 +37,22 @@ Mainnet stays off until `mainnet: { enabled: true, acknowledged: MAINNET_ACKNOWL also refuses it while `mainnetProblems(config)` lists anything (placeholder rdns, no homepage, no extension key, no WalletConnect project id, a remote icon). Clip Wallet is pre-release and unaudited: a mainnet build moves real funds. +Platform settings, all optional: + +| Setting | Default | | +| --- | --- | --- | +| `languages` | all twelve (`LANGUAGES`) | what Settings → Language offers; the first is the fallback | +| `appId` | `rdns` | the desktop and phone app id | +| `desktop.appId` | `.desktop` | macOS bundle id, Windows AppUserModelID | +| `mobile.bundleId`, `mobile.androidPackage` | from `appId` | iOS bundle id, Android package | +| `scheme` | the wallet key | deep links: `://wc?uri=…` | +| `fees`, `usage` | `{ enabled: false }` | reserved for Clip Cloud's hosted mode; accepted, never acted on by the kit | + +`platformIds(config)` returns every id the platforms use (extension gecko id, desktop app id, product, executable and +artifact names, iOS bundle id, Android package, scheme, slug). `@clip-wallet/config/node` has `loadClipConfigSync(file)` +(evaluates a clip.config.ts in a child Node process: Node 22.18+, for Expo's app.config.ts and electron-builder) and the +build-environment helpers the kits share (`buildEnv`, `resolveBuildConfig`, `iconDataUri`, `openChecklistItems`). + Helpers: `validateConfig`, `walletKey`, `rdnsDomain`, `enabledFamilies`, `includesEvmChain`, `isMainnetEnabled`, `defaults`. diff --git a/packages/config/src/node.ts b/packages/config/src/node.ts index 31bc221..d7d4f80 100644 --- a/packages/config/src/node.ts +++ b/packages/config/src/node.ts @@ -1,6 +1,8 @@ /** * Node-only helpers for build tools that need clip.config.ts outside a bundler: Expo's app.config.ts, * electron-builder's config file, create-clip-wallet. "@clip-wallet/config/node"; the main entry stays environment-free. + * + * @module */ import { execFileSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; diff --git a/packages/create-clip-wallet/README.md b/packages/create-clip-wallet/README.md index 95459d4..10cf26b 100644 --- a/packages/create-clip-wallet/README.md +++ b/packages/create-clip-wallet/README.md @@ -1,8 +1,9 @@ # create-clip-wallet -Start your own non-custodial wallet on the [Clip Wallet](https://github.com/ColdAI-org/clip-wallet) kit: a branded -browser extension for 14 network families, with its own name, icon, extension id and EIP-6963 rdns, plus a -Scaffold-HBAR dapp that connects to it on Hedera testnet. Test networks by default. +Start your own non-custodial wallet on the [Clip Wallet](https://github.com/ColdAI-org/clip-wallet) kit, on every +platform, from one `clip.config.ts`: a browser extension, a desktop app for macOS, Windows and Linux, and a phone app for +iOS and Android, for 14 network families, plus (if you want it) a Scaffold-HBAR dapp that connects to it on Hedera +testnet. > Pre-release: Clip Wallet runs on test networks only and has had no external audit. @@ -15,67 +16,124 @@ npx create-clip-wallet my-wallet ## Example ```sh -npx create-clip-wallet my-wallet -# or, without questions -npx create-clip-wallet my-wallet --name "Acme Wallet" --rdns com.acme.wallet --accent "#0B7A3B" \ - --networks "evm:*,hedera,solana,bitcoin" --homepage https://wallet.acme.example --yes -cd my-wallet && pnpm install && pnpm extension:build && pnpm next:dev +npx create-clip-wallet my-wallet --platforms extension,desktop,mobile --name "Acme Wallet" --rdns com.acme.wallet \ + --id com.acme.wallet --logo ./logo.png --accent "#0B7A3B" --networks "evm:*,hedera,solana,bitcoin" \ + --languages en,de,ja --homepage https://wallet.acme.example --yes +cd my-wallet && pnpm install +pnpm dev:extension # the browser extension +pnpm dev:desktop # the desktop app +pnpm --filter mobile start # the phone app (a development build) ``` -It makes **the same project** as the Scaffold-HBAR template followed by its identity step: - -```sh -npm create scaffold-hbar@latest -- --template ColdAI-org/scaffold-hbar-clip-wallet -cd && pnpm install && pnpm wallet:identity --name "Acme Wallet" --rdns com.acme.wallet -``` +| | Folder | On | +| --- | --- | --- | +| Browser extension | `packages/extension` | `@clip-wallet/extension-kit` (WXT) | +| Desktop app | `packages/desktop` | `@clip-wallet/desktop-kit` (Electron) | +| Phone app | `packages/mobile` | `@clip-wallet/mobile-kit` (Expo) | +| Scaffold-HBAR dapp (`--scaffold-hbar`) | `packages/nextjs` | Next.js, `@clip-wallet/connect` | -Both copy the template the same way (create-scaffold-hbar's copy step: skip `.git`/`node_modules`/`.env`, apply -`template.json`'s rename map, delete it, `git init`), then write the identity. +Each platform depends only on published `@clip-wallet/*` packages, pinned to one exact, provenance-signed version. ## Documentation - [Launch your own wallet](https://coldai.org/clip/docs/kit/) - [create-clip-wallet](https://coldai.org/clip/docs/kit/create-clip-wallet.html) +- [Stores and code signing](https://coldai.org/clip/docs/kit/signing.html) - [CLI reference](https://coldai.org/clip/docs/reference/cli.html) -## What the identity step writes +## Options | | | | --- | --- | -| `packages/extension/wallet.identity.json` | name, description, rdns (EIP-6963), homepage, icon, `extension.key` | -| `packages/extension/.keys/extension.pem` | the extension's private key: 0600, gitignored, never printed; the public half fixes the Chrome extension id | -| `packages/extension/icon.svg`, `public/icon/*.png` | a starter mark in your accent colour, or your `--icon` | -| `packages/extension/clip.config.ts` | `theme.accent` and `networks`, when given | -| `packages/extension/.env`, `packages/nextjs/.env.local` | your WalletConnect project id (`--walletconnect-project-id`), never committed | -| `docs/listings/` | listing-submission drafts for this identity: EIP-6963, WalletConnect Explorer and, for the families you turn on, TON Connect, NEAR, Stellar Wallets Kit, Tezos Beacon, Algorand use-wallet | +| `--platforms extension,desktop,mobile` | which apps (default: all three) | +| `--scaffold-hbar` | also the Scaffold-HBAR demo dapp (default: off) | +| `--name "Acme Wallet"` | the name on every platform (default: from the folder) | +| `--rdns com.acme.wallet` | EIP-6963 id, a reverse domain you own (default: a `com.example.*` placeholder) | +| `--id com.acme.wallet` | desktop and phone app id: macOS `.desktop`, iOS bundle id, Android package (default: the rdns) | +| `--logo ./logo.png` | your logo (`.png`, ideally 1024×1024, or `.svg`); every icon is rendered from it (default: a starter mark) | +| `--accent "#0B7A3B"` | accent colour; also the phone app's icon and splash background | +| `--networks "evm:*,hedera"` | `evm:*`, `evm:`, hedera, solana, bitcoin, sui, aptos, cardano, substrate, starknet, ton, near, stellar, tezos, algorand | +| `--languages en,de,ja` | en, de, fr, es, pt-BR, it, tr, ja, ko, zh-Hans, ar, hi (default: all; the first is the fallback) | +| `--homepage`, `--description` | your website, one sentence for the stores | +| `--walletconnect-project-id` | written to `.env`, never committed | +| `--new-key`, `--no-git`, `--root `, `--yes` | new extension key (identity), no `git init`, project for in-project commands, no questions | + +Without `--yes` it asks for what the flags leave out and asks again after an answer `@clip-wallet/config` rejects. + +## What it writes + +| | | +| --- | --- | +| `clip.config.ts` | the one config: theme, networks, languages, routing, services, mainnet (`false`) | +| `wallet.identity.json` | name, description, rdns (EIP-6963), homepage, icon, appId, `extension.key` | +| `.keys/extension.pem` | the extension's private key: 0600, gitignored, never printed; the public half fixes the Chrome extension id | +| `icon.svg` / `icon.png` and every platform's icons | extension 16–128; macOS `.icns`, Windows `.ico`, Linux 16–1024, tray (and the macOS template); iOS icon, Android adaptive and monochrome icons, splash | +| extension page titles | the name | +| `.env`, `packages/nextjs/.env.local` | your WalletConnect project id, never committed | +| `docs/listings/` | listing-submission drafts: EIP-6963, WalletConnect Explorer and, for the families you turn on, TON Connect, NEAR, Stellar Wallets Kit, Tezos Beacon, Algorand use-wallet | +| `docs/signing.md` | code signing and store accounts per platform: what each needs and the variable names the tools read | It refuses Clip Wallet's own identity (`org.coldai.*`, the name "Clip Wallet") and anything `@clip-wallet/config` rejects, and writes nothing then. Run again, it keeps the extension key unless you pass `--new-key`. +**Icons with Node alone.** PNG decoding and encoding, area-average resampling with premultiplied alpha, and the ICO and +ICNS containers are written here with Node built-ins. A PNG logo needs nothing else. An SVG logo is rasterised once at +1024 px by the first of these that is installed: `@resvg/resvg-js` (in the project), `rsvg-convert` (librsvg), `sips` +(macOS, built in), `magick` (ImageMagick). Without any of them, pass a PNG. + ## Commands ``` create-clip-wallet [options] new project -create-clip-wallet identity [options] in a project (pnpm wallet:identity) +create-clip-wallet identity [options] in a project (pnpm wallet:identity): name, ids, accent, logo, networks, languages +create-clip-wallet brand [--logo …] in a project (pnpm wallet:brand): every platform's icons from the logo create-clip-wallet listings in a project (pnpm wallet:listings) create-clip-wallet mainnet-check in a project (pnpm wallet:mainnet-check): exit 0 ready, 2 work left, 1 mainnet on but not ready ``` -Options: `--name`, `--rdns`, `--accent`, `--networks`, `--homepage`, `--description`, `--icon`, -`--walletconnect-project-id`, `--new-key`, `--no-git`, `--root `, `--yes`. Run with `--help` for details. +In a project, per platform: + +| | | +| --- | --- | +| Extension | `pnpm dev:extension` · `pnpm extension:build` (load `.output/chrome-mv3` unpacked) · `pnpm extension:zip` (store zip) | +| Desktop | `pnpm dev:desktop` · `pnpm desktop:build` · `pnpm desktop:dist` (this OS) · `desktop:dist:mac` / `:win` / `:linux` (electron-builder) | +| Phone | `pnpm --filter mobile start` · `pnpm mobile:prebuild` then `pnpm --filter mobile ios` / `android` · `pnpm mobile:export` (JS bundles) · `pnpm --filter mobile eas:build` (EAS) | +| All | `pnpm harness` · `pnpm check-types` · `pnpm build` | + +No account or certificate is needed to build anything above. Store accounts and signing (Chrome Web Store, Apple +Developer ID and notarization, Windows code signing, App Store, Google Play) are only for shipping; `docs/signing.md` +in the project lists them, with environment variable names and no secrets. + +## Same project as the Scaffold-HBAR template + +```sh +npm create scaffold-hbar@latest -- --template ColdAI-org/scaffold-hbar-clip-wallet +cd && pnpm install && pnpm wallet:identity --name "Acme Wallet" --rdns com.acme.wallet +``` + +gives exactly what `npx create-clip-wallet --scaffold-hbar --name "Acme Wallet" --rdns com.acme.wallet --yes` +gives (every platform plus the dapp): both copy the template the same way (create-scaffold-hbar's copy step: skip +`.git`/`node_modules`/`.env`, apply `template.json`'s rename map, delete it, `git init`), then write the identity. +With fewer `--platforms`, or without `--scaffold-hbar`, create-clip-wallet then removes those parts: their folders, their +root scripts, and their sections of the docs and the CI workflow (marked `platform:` in the template). ## Testnet first New wallets run on test networks. There is no `--mainnet`: mainnet is a later decision by the wallet's owner, after -`packages/extension/MAINNET.md`. `pnpm harness` and the extension build both refuse mainnet while a box is open, and the -build also while `mainnetProblems()` (placeholder rdns, no homepage, no extension key, no WalletConnect project id, a -remote icon) lists anything. +`MAINNET.md`. `pnpm harness` and every platform's build refuse mainnet while a box is open, and the builds also while +`mainnetProblems()` (placeholder rdns, no homepage, no extension key, no WalletConnect project id, a remote icon) lists +anything. `clip.config.ts` also accepts `fees` and `usage`, reserved for Clip Cloud's hosted mode: off by default and +never acted on by the kit. ## Versioning and provenance Published from [ColdAI-org/clip-wallet](https://github.com/ColdAI-org/clip-wallet) by CI with npm provenance: every tarball is signed and traceable to the commit that built it (`npm audit signatures` checks it). All `@clip-wallet/*` -packages share one version; pin it exactly. +packages share one version; pin it exactly. The projects create-clip-wallet makes pin that version exactly; +`pnpm verify:provenance` checks every kit package's attestation. + +The template (`template/` in the package) is bundled at `prepack` from `templates/scaffold-hbar-clip-wallet` in the +monorepo; files npm won't pack (`.gitignore`, `.npmrc`) travel as `_gitignore` / `_npmrc` and are renamed back. ## Licence diff --git a/packages/desktop-kit/README.md b/packages/desktop-kit/README.md index fac2c3e..531c9bf 100644 --- a/packages/desktop-kit/README.md +++ b/packages/desktop-kit/README.md @@ -5,13 +5,24 @@ The Clip Wallet desktop app (Electron: macOS, Windows, Linux) as a library. A wa process (vault, engine, approvals, Touch ID, Ledger over WebHID, linked devices), the sandboxed preloads, the built-in dapp browser with 1Mask for all 14 network families and the pages come from here, versioned and signed. +> Pre-release: Clip Wallet runs on test networks only and has had no external audit. + +## Install + +```sh +npm i @clip-wallet/desktop-kit @clip-wallet/config electron electron-vite vite react react-dom +``` + +## Example + ```ts -// electron.vite.config.ts -import { defineConfig } from "electron-vite"; +// electron.vite.config.ts (in a create-clip-wallet project: import clipConfig from "../../clip.config", configDir: "../..") +import { defineConfig as defineClipConfig } from "@clip-wallet/config"; import { clipDesktop } from "@clip-wallet/desktop-kit/electron-vite"; -import clipConfig from "../../clip.config"; -export default defineConfig(clipDesktop({ config: clipConfig, root: import.meta.dirname, configDir: "../.." })); +const clipConfig = defineClipConfig({ name: "Acme Wallet", rdns: "com.acme.wallet", networks: ["evm:*", "hedera", "solana"] }); + +export default clipDesktop({ config: clipConfig, root: import.meta.dirname }); ``` ```js @@ -54,11 +65,18 @@ Developer ID Application), `APPLE_API_KEY` / `APPLE_API_KEY_ID` / `APPLE_API_ISS Peer dependencies: `electron`, `electron-vite`, `vite`, `react`, `react-dom`. Node 22.18 or newer for the build helpers. +## Documentation + +- [Launch your own wallet](https://coldai.org/clip/docs/kit/) +- [Build and ship: the desktop app](https://coldai.org/clip/docs/kit/build-and-ship.html#desktop-app) +- [Stores and code signing](https://coldai.org/clip/docs/kit/signing.html) +- [API reference](https://coldai.org/clip/docs/reference/api/desktop-kit.html) + ## Versioning and provenance Published from [ColdAI-org/clip-wallet](https://github.com/ColdAI-org/clip-wallet) by CI with npm provenance: every tarball is signed and traceable to the commit that built it (`npm audit signatures` checks it). All `@clip-wallet/*` -packages share one version; pin it exactly. Pre-release: test networks by default. +packages share one version; pin it exactly. ## Licence diff --git a/packages/desktop-kit/src/builder.ts b/packages/desktop-kit/src/builder.ts index 6cbe7e4..ceeea0c 100644 --- a/packages/desktop-kit/src/builder.ts +++ b/packages/desktop-kit/src/builder.ts @@ -17,6 +17,8 @@ * Windows signing WIN_CSC_LINK + WIN_CSC_KEY_PASSWORD (or CSC_LINK on a Windows runner) * Auto-update metadata only for CLIP_UPDATES=1 builds, published to CLIP_UPDATES_OWNER / CLIP_UPDATES_REPO on GitHub. * Pure apart from reading clip.config.ts when only configFile is given (exported for tests). + * + * @module */ import { resolve } from "node:path"; import { platformIds, type ClipConfig } from "@clip-wallet/config"; diff --git a/packages/desktop-kit/src/csp.ts b/packages/desktop-kit/src/csp.ts index 3a5e017..83b325e 100644 --- a/packages/desktop-kit/src/csp.ts +++ b/packages/desktop-kit/src/csp.ts @@ -2,6 +2,8 @@ * The wallet and approval windows' Content-Security-Policy: sent as a header by the clip-app: protocol * (main/app-protocol.ts) and written into the pages' meta tag at build time (electron-vite.ts). No imports, so the * build config can load it from source. + * + * @module */ export function walletCsp(mediaProxyUrl?: string): string { const media = mediaProxyUrl ? ` ${new URL(mediaProxyUrl).origin}` : ""; diff --git a/packages/desktop-kit/src/electron-vite.ts b/packages/desktop-kit/src/electron-vite.ts index 7959bab..8c42cee 100644 --- a/packages/desktop-kit/src/electron-vite.ts +++ b/packages/desktop-kit/src/electron-vite.ts @@ -17,6 +17,8 @@ * CLIP_UPDATES=1 turn electron-updater on (signed release builds only) * CLIP_EXTENSION_IDS Chromium extension ids allowed to use the app over native messaging * Runs in Node (electron-vite loads it). + * + * @module */ import { createRequire } from "node:module"; import { existsSync, mkdirSync } from "node:fs"; diff --git a/packages/desktop-kit/src/index.ts b/packages/desktop-kit/src/index.ts index ed8817d..f9f758a 100644 --- a/packages/desktop-kit/src/index.ts +++ b/packages/desktop-kit/src/index.ts @@ -11,6 +11,8 @@ * "@clip-wallet/desktop-kit/preload/dapp" (side effect) the dapp-tab preload (1Mask) * "@clip-wallet/desktop-kit/renderer" mountWallet(), mountApproval(), mountBrowserChrome() * "@clip-wallet/desktop-kit/native-host" (side effect) the native-messaging host program + * + * @module */ export { parseDeepLink, deepLinkFromArgv, type DeepLink } from "./main/deeplink"; export { walletCsp } from "./main/app-paths"; diff --git a/packages/desktop-kit/src/main/index.ts b/packages/desktop-kit/src/main/index.ts index 7063c80..bf122ab 100644 --- a/packages/desktop-kit/src/main/index.ts +++ b/packages/desktop-kit/src/main/index.ts @@ -9,6 +9,8 @@ * Security switches are set before anything loads: single instance, sandbox for every renderer, no remote content * in app windows (CSP + a request filter on the default session), no new windows, no , no navigation of * app windows, permission handlers that deny by default. + * + * @module */ import { app, diff --git a/packages/desktop-kit/src/native-host/main.ts b/packages/desktop-kit/src/native-host/main.ts index f11557a..a9a8aab 100644 --- a/packages/desktop-kit/src/native-host/main.ts +++ b/packages/desktop-kit/src/native-host/main.ts @@ -4,6 +4,8 @@ * browser's stdio and Clip Desktop's per-user socket; frames are end-to-end encrypted after pairing. * Bundled by scripts/build-native-host.mjs to out/native-host/clip-native-host.cjs and run with the app's own * executable as Node (src/main/native-hosts.ts writes the launcher). + * + * @module */ import { nativeHostMain } from "@clip-wallet/link/node"; diff --git a/packages/desktop-kit/src/preload/dapp.ts b/packages/desktop-kit/src/preload/dapp.ts index 6b17e50..58fcdfb 100644 --- a/packages/desktop-kit/src/preload/dapp.ts +++ b/packages/desktop-kit/src/preload/dapp.ts @@ -10,6 +10,8 @@ * * Nothing is exposed to the page: no contextBridge.exposeInMainWorld, so window.require, ipcRenderer, process and * electron stay out of its reach. + * + * @module */ import { contextBridge, ipcRenderer } from "electron"; import { createContentBridge, type RuntimePort } from "@clip-wallet/1mask/content"; diff --git a/packages/desktop-kit/src/preload/wallet.ts b/packages/desktop-kit/src/preload/wallet.ts index a927899..0968b50 100644 --- a/packages/desktop-kit/src/preload/wallet.ts +++ b/packages/desktop-kit/src/preload/wallet.ts @@ -2,6 +2,8 @@ * Preload of the wallet, approval and browser-toolbar windows (sandboxed, isolated world). It exposes one small, * typed bridge object; the renderer never gets ipcRenderer itself. Every call is checked again in the main * process (schema, then sender: ipc-guard.ts). + * + * @module */ import { contextBridge, ipcRenderer } from "electron"; import { CH, type ChromeCall, type ChromeState, type DesktopCall, type HidJob, type HidReply } from "../shared/ipc"; diff --git a/packages/desktop-kit/src/renderer/index.ts b/packages/desktop-kit/src/renderer/index.ts index 5685c56..2322517 100644 --- a/packages/desktop-kit/src/renderer/index.ts +++ b/packages/desktop-kit/src/renderer/index.ts @@ -3,6 +3,8 @@ * wallet mountWallet() the wallet window (the extension's screens from @clip-wallet/ui) * approval mountApproval() the approval window * browser mountBrowserChrome() the built-in browser's toolbar + * + * @module */ export { mountApproval, mountWallet } from "./shared/mount"; export { mountBrowserChrome } from "./browser/mount"; diff --git a/packages/mobile-kit/README.md b/packages/mobile-kit/README.md index 67562fd..b2dc0bd 100644 --- a/packages/mobile-kit/README.md +++ b/packages/mobile-kit/README.md @@ -5,6 +5,19 @@ The Clip Wallet phone app (Expo / React Native: iOS, Android) as a library. A wa engine host (Keychain / Keystore, Face ID / fingerprint, passkeys), the in-app dapp browser with 1Mask, WalletConnect, Clip Plugins, Ledger over Bluetooth and Keystone, linked devices and notifications come from here. +> Pre-release: Clip Wallet runs on test networks only and has had no external audit. + +## Install + +```sh +npx expo install @clip-wallet/mobile-kit @clip-wallet/config +``` + +Then add the native modules listed in this package's `peerDependencies` (Expo autolinking only links what the app +depends on). + +## Example + ```ts // index.ts import "@clip-wallet/mobile-kit/polyfills"; // first @@ -48,11 +61,18 @@ this package's `peerDependencies`. Node 22.18 or newer (the helpers read clip.co `expo export` (the JS bundle) and `expo prebuild` (the native projects, generated, never committed) need no account. `eas build` needs an Expo account and, for store builds, Apple Developer / Google Play credentials. +## Documentation + +- [Launch your own wallet](https://coldai.org/clip/docs/kit/) +- [Build and ship: the phone app](https://coldai.org/clip/docs/kit/build-and-ship.html#phone-app) +- [Stores and code signing](https://coldai.org/clip/docs/kit/signing.html) +- [API reference](https://coldai.org/clip/docs/reference/api/mobile-kit.html) + ## Versioning and provenance Published from [ColdAI-org/clip-wallet](https://github.com/ColdAI-org/clip-wallet) by CI with npm provenance: every tarball is signed and traceable to the commit that built it (`npm audit signatures` checks it). All `@clip-wallet/*` -packages share one version; pin it exactly. Pre-release: test networks by default. +packages share one version; pin it exactly. ## Licence diff --git a/packages/mobile-kit/src/expo.ts b/packages/mobile-kit/src/expo.ts index 0866bef..1863e19 100644 --- a/packages/mobile-kit/src/expo.ts +++ b/packages/mobile-kit/src/expo.ts @@ -13,6 +13,8 @@ * Optional build-time env (never committed): CLIP_ASSOCIATED_DOMAIN (universal links + passkeys; needs a signed build * and the domain's AASA / assetlinks files), CLIP_WALLETCONNECT_PROJECT_ID or EXPO_PUBLIC_WC_PROJECT_ID (WalletConnect). * Node only, synchronous (Expo evaluates app.config.ts as CommonJS). No relative imports: build tools load it from source. + * + * @module */ import { existsSync, readFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; diff --git a/packages/mobile-kit/src/index.ts b/packages/mobile-kit/src/index.ts index 57bb980..85c1b72 100644 --- a/packages/mobile-kit/src/index.ts +++ b/packages/mobile-kit/src/index.ts @@ -10,6 +10,8 @@ * * "@clip-wallet/mobile-kit/expo" expoConfig({ configFile }): app.config.ts from clip.config.ts (Node) * "@clip-wallet/mobile-kit/metro" withClipWallet(metroConfig, { configFile }): Metro wiring (Node, CommonJS) + * + * @module */ // Background notification checks must be defined at load (expo-task-manager). import "./background/background-task"; diff --git a/packages/mobile-kit/src/plugins/gunzip.ts b/packages/mobile-kit/src/plugins/gunzip.ts index a0bfb7a..a5a5977 100644 --- a/packages/mobile-kit/src/plugins/gunzip.ts +++ b/packages/mobile-kit/src/plugins/gunzip.ts @@ -1,3 +1,4 @@ +/// /** * gunzip for plugin installs on React Native: Hermes has no DecompressionStream, so @clip-wallet/plugins takes this * through `NpmOptions.gunzip`. pako 2.2.0 `Inflate` (windowBits 16+15 = gzip wrapper), stopping as soon as the diff --git a/packages/mobile-kit/src/polyfills.ts b/packages/mobile-kit/src/polyfills.ts index 44a74fc..dd9a98f 100644 --- a/packages/mobile-kit/src/polyfills.ts +++ b/packages/mobile-kit/src/polyfills.ts @@ -3,6 +3,8 @@ * crypto.getRandomValues (react-native-get-random-values), TextEncoder/TextDecoder (fast-text-encoding), * URL (react-native-url-polyfill), Buffer, atob/btoa. We add crypto.randomUUID (expo-crypto) and a * structuredClone fallback for JSON-shaped data where the engine (Hermes) lacks them. + * + * @module */ import "@walletconnect/react-native-compat"; import { randomUUID } from "expo-crypto"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4492191..9149c89 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -310,6 +310,9 @@ importers: '@clip-wallet/core': specifier: workspace:* version: link:../../packages/core + '@clip-wallet/desktop-kit': + specifier: workspace:* + version: link:../../packages/desktop-kit '@clip-wallet/engine': specifier: workspace:* version: link:../../packages/engine @@ -334,6 +337,9 @@ importers: '@clip-wallet/media-client': specifier: workspace:* version: link:../../packages/media-client + '@clip-wallet/mobile-kit': + specifier: workspace:* + version: link:../../packages/mobile-kit '@clip-wallet/names': specifier: workspace:* version: link:../../packages/names From f4c23885ab92d59752de347e9de57376ddda6827 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:39:54 +0800 Subject: [PATCH 4/6] fix: CodeQL findings in the code this branch touches (ReDoS, regex escaping, a file-system race) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Linear scans instead of polynomial regexes on input: trimming hyphens in slugOfName() and platformIds() (@clip-wallet/config), the networks and languages arrays create-clip-wallet writes into clip.config.ts, the platform markers stripParts() reads (matched on the trimmed line), the "-u-…nu-" check in numberLocale() (@clip-wallet/i18n), and the trailing slashes of the media proxy base (@clip-wallet/media-client). - tools/harness/check.mjs (and the template's byte-for-byte copy) and tools/kit/e2e.mjs escape every regex metacharacter, backslash included, when they build a RegExp from a binding, module or app id. - selectParts() reads each marked file and skips one that isn't there, instead of checking first. Co-Authored-By: Claude Opus 5.5 --- packages/config/src/index.ts | 24 +++++++++++++------ packages/create-clip-wallet/src/identity.mjs | 23 ++++++++++++++++-- packages/create-clip-wallet/src/template.mjs | 14 ++++++++--- packages/i18n/src/numbers.ts | 3 ++- packages/media-client/src/index.ts | 4 +++- .../tools/harness/check.mjs | 7 ++++-- tools/harness/check.mjs | 7 ++++-- tools/kit/e2e.mjs | 2 +- 8 files changed, 65 insertions(+), 19 deletions(-) diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 62d15a7..9a2fc09 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -389,15 +389,25 @@ export function mainnetProblems(config: ClipConfig, env: Record a && s[b - 1] === "-") b--; + return s.slice(a, b); +} + /** "Acme Wallet" -> "acme-wallet": file-safe, for executables, packages and artifact names. */ export function slugOfName(name: string): string { return ( - name - .toLowerCase() - .normalize("NFKD") - .replace(/[̀-ͯ]/g, "") - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, "") || "wallet" + trimHyphens( + name + .toLowerCase() + .normalize("NFKD") + .replace(/[̀-ͯ]/g, "") + .replace(/[^a-z0-9]+/g, "-"), + ) || "wallet" ); } @@ -437,7 +447,7 @@ export interface PlatformIds { export function platformIds(config: Pick): PlatformIds { const appId = config.appId ?? config.rdns; const slug = slugOfName(config.name); - const fileName = config.name.replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "Wallet"; + const fileName = trimHyphens(config.name.replace(/[^A-Za-z0-9]+/g, "-")) || "Wallet"; return { name: config.name, appId, diff --git a/packages/create-clip-wallet/src/identity.mjs b/packages/create-clip-wallet/src/identity.mjs index 194b413..d542a60 100644 --- a/packages/create-clip-wallet/src/identity.mjs +++ b/packages/create-clip-wallet/src/identity.mjs @@ -105,6 +105,25 @@ export function isStarterSvg(text) { return /^<\/svg>\s*$/.test(text); } +/** + * The first `key: [ … ]` array literal in a config file, replaced with `items` (a scan, not a regex: linear on any input). + * @param {string} text @param {string} key @param {readonly string[]} items + */ +function setArray(text, key, items) { + for (let from = 0; ; ) { + const i = text.indexOf(`${key}:`, from); + if (i < 0) return text; + let j = i + key.length + 1; + while (j < text.length && /\s/.test(text[j] ?? "")) j++; + if (text[j] === "[") { + const end = text.indexOf("]", j); + if (end < 0) return text; + return `${text.slice(0, i)}${key}: [${items.map((n) => JSON.stringify(n)).join(", ")}]${text.slice(end + 1)}`; + } + from = i + 1; + } +} + /** @param {string} file @param {(text: string) => string} fn */ function edit(file, fn) { if (!existsSync(file)) return; @@ -200,8 +219,8 @@ export function applyIdentity(root, answers) { edit(join(root, CONFIG_FILE), (t) => { let out = t; if (answers.accent) out = out.replace(/accent:\s*"#[0-9a-fA-F]{3,6}"/, `accent: ${JSON.stringify(answers.accent)}`); - if (answers.networks) out = out.replace(/networks:\s*\[[^\]]*\]/, `networks: [${answers.networks.map((n) => JSON.stringify(n)).join(", ")}]`); - if (answers.languages) out = out.replace(/languages:\s*\[[^\]]*\]/, `languages: [${answers.languages.map((n) => JSON.stringify(n)).join(", ")}]`); + if (answers.networks) out = setArray(out, "networks", answers.networks); + if (answers.languages) out = setArray(out, "languages", answers.languages); return out; }); for (const page of ["popup", "tab"]) { diff --git a/packages/create-clip-wallet/src/template.mjs b/packages/create-clip-wallet/src/template.mjs index 097609a..09ff3b9 100644 --- a/packages/create-clip-wallet/src/template.mjs +++ b/packages/create-clip-wallet/src/template.mjs @@ -142,8 +142,9 @@ export function stripParts(text, keep) { /** @type {string[]} */ const stack = []; for (const line of lines) { - const open = /^\s*(?:)?\s*$/.exec(line); - const close = /^\s*(?:)?\s*$/.exec(line); + const bare = line.trim(); + const open = /^(?:)?$/.exec(bare); + const close = /^(?:)?$/.exec(bare); const dropping = stack.some((p) => !keep.has(p)); if (open) { stack.push(/** @type {string} */ (open[1])); @@ -213,7 +214,14 @@ export function selectParts(projectDir, o) { writeFileSync(pkgFile, `${JSON.stringify(pkg, null, 2)}\n`); for (const f of MARKED_FILES) { const file = join(projectDir, f); - if (existsSync(file)) writeFileSync(file, stripParts(readFileSync(file, "utf8"), keep)); + let text; + try { + text = readFileSync(file, "utf8"); + } catch (e) { + if (/** @type {NodeJS.ErrnoException} */ (e).code === "ENOENT") continue; // a part's file that isn't in this project + throw e; + } + writeFileSync(file, stripParts(text, keep)); } return removed; } diff --git a/packages/i18n/src/numbers.ts b/packages/i18n/src/numbers.ts index 1036132..01f963d 100644 --- a/packages/i18n/src/numbers.ts +++ b/packages/i18n/src/numbers.ts @@ -8,7 +8,8 @@ /** The Intl locale for numbers: Latin digits everywhere. */ export function numberLocale(locale: string): string { - if (/-u-.*nu-/.test(locale)) return locale; + const ext = locale.indexOf("-u-"); + if (ext >= 0 && locale.indexOf("nu-", ext + 3) >= 0) return locale; // a numbering system is already chosen return locale.split("-")[0] === "ar" ? `${locale}-u-nu-latn` : locale; } diff --git a/packages/media-client/src/index.ts b/packages/media-client/src/index.ts index dca9925..76f68dc 100644 --- a/packages/media-client/src/index.ts +++ b/packages/media-client/src/index.ts @@ -218,7 +218,9 @@ export function mediaProxyUrl(base: string | null | undefined, raw: string | nul if (!source) return null; const kind = opts.kind ?? guessKind(source.url); if (!kind) return null; - const root = base.replace(/\/+$/, ""); + let end = base.length; + while (end > 0 && base[end - 1] === "/") end--; + const root = base.slice(0, end); return { kind, src: `${root}/v1/media?src=${encodeURIComponent(source.url)}&kind=${kind}` }; } diff --git a/templates/scaffold-hbar-clip-wallet/tools/harness/check.mjs b/templates/scaffold-hbar-clip-wallet/tools/harness/check.mjs index 616b6fe..7df792c 100644 --- a/templates/scaffold-hbar-clip-wallet/tools/harness/check.mjs +++ b/templates/scaffold-hbar-clip-wallet/tools/harness/check.mjs @@ -49,6 +49,9 @@ export const VAULT_IMPORT_ALLOW = [ /^(?:packages\/ui|apps\/extension)\/(?:.*\/)?onboarding(?:\/|\.[cm]?[jt]sx?$)/i, ]; +/** A string as a literal inside a RegExp (every metacharacter escaped, backslash included). */ +const escapeRegExp = (s) => s.replace(/[\\^$.*+?()[\]{}|/]/g, "\\$&"); + /** * Crypto-critical dependencies (audit SUP-02): pinned to an exact version in every workspace package, so a lockfile * regeneration can't pull in a new patch release of the code that derives keys, hashes and verifies signatures. @@ -284,7 +287,7 @@ function keyMaterialHits(imp, code) { let hit = false; for (const b of bs) { if (!b.namespace && rule.symbols.includes(b.imported)) hit = true; - const name = b.name.replace(/\$/g, "\\$"); + const name = escapeRegExp(b.name); const sym = rule.symbols.join("|"); // binding.sign(…), binding.utils.randomSecretKey(…), ns.secp256k1.sign(…) if (new RegExp(`\\b${name}\\s*(?:\\.\\s*[\\w$]+\\s*)?\\.\\s*(?:utils\\s*\\.\\s*)?(?:${sym})\\b`).test(code)) hit = true; @@ -580,7 +583,7 @@ function kitChecks(root, files, parsed, fail, warn) { const code = f ? f.lexed.code : raw; const imported = (f && findImports(f.src, f.lexed).some((i) => i.module === b.module && new RegExp(`\\b${b.fn}\\b`).test(i.clause))) || - new RegExp(`\\b${b.fn}\\b[^;]*=\\s*require\\(\\s*["']${b.module.replace(/\//g, "\\/")}["']\\s*\\)`).test(raw); + new RegExp(`\\b${b.fn}\\b[^;]*=\\s*require\\(\\s*["']${escapeRegExp(b.module)}["']\\s*\\)`).test(raw); if (!imported || !new RegExp(`\\b${b.fn}\\s*\\(`).test(code)) { fail("kit-security", b.file, 0, `${b.file.split("/").pop()} must build ${b.what} with ${b.fn}() from ${b.module}: it carries the wallet's config, the security floor and the mainnet checklist. Restore it from the template.`); } diff --git a/tools/harness/check.mjs b/tools/harness/check.mjs index 616b6fe..7df792c 100644 --- a/tools/harness/check.mjs +++ b/tools/harness/check.mjs @@ -49,6 +49,9 @@ export const VAULT_IMPORT_ALLOW = [ /^(?:packages\/ui|apps\/extension)\/(?:.*\/)?onboarding(?:\/|\.[cm]?[jt]sx?$)/i, ]; +/** A string as a literal inside a RegExp (every metacharacter escaped, backslash included). */ +const escapeRegExp = (s) => s.replace(/[\\^$.*+?()[\]{}|/]/g, "\\$&"); + /** * Crypto-critical dependencies (audit SUP-02): pinned to an exact version in every workspace package, so a lockfile * regeneration can't pull in a new patch release of the code that derives keys, hashes and verifies signatures. @@ -284,7 +287,7 @@ function keyMaterialHits(imp, code) { let hit = false; for (const b of bs) { if (!b.namespace && rule.symbols.includes(b.imported)) hit = true; - const name = b.name.replace(/\$/g, "\\$"); + const name = escapeRegExp(b.name); const sym = rule.symbols.join("|"); // binding.sign(…), binding.utils.randomSecretKey(…), ns.secp256k1.sign(…) if (new RegExp(`\\b${name}\\s*(?:\\.\\s*[\\w$]+\\s*)?\\.\\s*(?:utils\\s*\\.\\s*)?(?:${sym})\\b`).test(code)) hit = true; @@ -580,7 +583,7 @@ function kitChecks(root, files, parsed, fail, warn) { const code = f ? f.lexed.code : raw; const imported = (f && findImports(f.src, f.lexed).some((i) => i.module === b.module && new RegExp(`\\b${b.fn}\\b`).test(i.clause))) || - new RegExp(`\\b${b.fn}\\b[^;]*=\\s*require\\(\\s*["']${b.module.replace(/\//g, "\\/")}["']\\s*\\)`).test(raw); + new RegExp(`\\b${b.fn}\\b[^;]*=\\s*require\\(\\s*["']${escapeRegExp(b.module)}["']\\s*\\)`).test(raw); if (!imported || !new RegExp(`\\b${b.fn}\\s*\\(`).test(code)) { fail("kit-security", b.file, 0, `${b.file.split("/").pop()} must build ${b.what} with ${b.fn}() from ${b.module}: it carries the wallet's config, the security floor and the mainnet checklist. Restore it from the template.`); } diff --git a/tools/kit/e2e.mjs b/tools/kit/e2e.mjs index 227af63..5cb0d8a 100644 --- a/tools/kit/e2e.mjs +++ b/tools/kit/e2e.mjs @@ -265,7 +265,7 @@ try { const iosDir = readdirSync(join(mob, "ios")).find((d) => d.endsWith(".xcodeproj"))?.replace(/\.xcodeproj$/, ""); check(!!iosDir, `ios/${iosDir}.xcodeproj generated`); const pbx = readFileSync(join(mob, "ios", `${iosDir}.xcodeproj`, "project.pbxproj"), "utf8"); - check(new RegExp(`PRODUCT_BUNDLE_IDENTIFIER = "?${APP_ID.replace(/\./g, "\\.")}"?;`).test(pbx), `iOS bundle id ${APP_ID}`); + check(new RegExp(`PRODUCT_BUNDLE_IDENTIFIER = "?${APP_ID.replace(/[\\^$.*+?()[\]{}|/]/g, "\\$&")}"?;`).test(pbx), `iOS bundle id ${APP_ID}`); const info = readFileSync(join(mob, "ios", iosDir, "Info.plist"), "utf8"); check(plistValue(info, "CFBundleDisplayName") === NAME && /acmewallet<\/string>/.test(info), "iOS display name and URL scheme"); check(/Unlock Acme Wallet with Face ID/.test(info), "iOS permission texts use the name"); From af3e8bd0e7513713fbdbcd3df84a2b51be8ec1d7 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:39:54 +0800 Subject: [PATCH 5/6] fix(ci): services install their workspace dependencies; desktop e2e and unit tests on every runner; repro bundle - ci.yml: pnpm --filter "{./services/**}..." (with braces) also installs packages/link and backup-client, which the services typecheck against; the old selector installed only the services. - desktop.yml: the Playwright step runs with --conditions=development, as `pnpm e2e` does locally (its helpers import workspace packages from source; nothing is built to dist/ in that job). - desktop-kit tests on Windows: the clip-app path test uses the platform's own paths, and the launcher's POSIX mode is checked off Windows only. - repro.yml: full history, so repro-check.sh's bundle of the commit can be fetched in each container. - create-clip-wallet scaffold tests get 60 s each: copying the template and rendering every icon takes several seconds on a shared runner. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 3 ++- .github/workflows/desktop.yml | 3 +++ .github/workflows/repro.yml | 4 +++- packages/create-clip-wallet/test/scaffold.test.ts | 5 ++++- packages/desktop-kit/test/native-hosts.test.ts | 3 ++- packages/desktop-kit/test/url-policy.test.ts | 15 +++++++++------ 6 files changed, 23 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a95d53c..0e66131 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,7 +122,8 @@ jobs: with: node-version: ${{ env.NODE_VERSION }} cache: pnpm - - run: pnpm install --frozen-lockfile --filter "./services/**..." + # Braces: the services and their workspace dependencies (packages/link, packages/backup-client). + - run: pnpm install --frozen-lockfile --filter "{./services/**}..." - name: Typecheck run: pnpm -r --filter "./services/**" typecheck - name: Test (inside workerd, local Miniflare D1/R2; nothing remote) diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml index fbb8fc5..589bc8a 100644 --- a/.github/workflows/desktop.yml +++ b/.github/workflows/desktop.yml @@ -78,6 +78,9 @@ jobs: - name: End-to-end (Playwright, real app) working-directory: apps/desktop + env: + # The e2e helpers import workspace packages from source (nothing is built to dist/ in this job). + NODE_OPTIONS: --conditions=development run: | if [ "$RUNNER_OS" = "Linux" ]; then xvfb-run --auto-servernum -- pnpm exec playwright test diff --git a/.github/workflows/repro.yml b/.github/workflows/repro.yml index d7a5bcd..3bad057 100644 --- a/.github/workflows/repro.yml +++ b/.github/workflows/repro.yml @@ -34,7 +34,9 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - fetch-depth: 1 + # Full history: the script bundles the commit for a clean clone in each container, and a bundle of a + # shallow commit can't be fetched (its parents are missing). + fetch-depth: 0 - name: repro-check env: REPRO_OUT: ${{ runner.temp }} diff --git a/packages/create-clip-wallet/test/scaffold.test.ts b/packages/create-clip-wallet/test/scaffold.test.ts index 270e03c..3abdfff 100644 --- a/packages/create-clip-wallet/test/scaffold.test.ts +++ b/packages/create-clip-wallet/test/scaffold.test.ts @@ -3,7 +3,10 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, import { tmpdir } from "node:os"; import { join, relative } from "node:path"; import { deflateSync } from "node:zlib"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +// Each project copies the template and renders every platform's icons: seconds on a shared CI runner, not milliseconds. +vi.setConfig({ testTimeout: 60_000 }); import { applyIdentity, copyTemplate, diff --git a/packages/desktop-kit/test/native-hosts.test.ts b/packages/desktop-kit/test/native-hosts.test.ts index ff5c5b1..486066d 100644 --- a/packages/desktop-kit/test/native-hosts.test.ts +++ b/packages/desktop-kit/test/native-hosts.test.ts @@ -33,7 +33,8 @@ describe("browser connector (native-messaging host registration)", () => { expect(chrome).toMatchObject({ name: "org.coldai.clipwallet.link", type: "stdio", path: launcherPath(e), allowed_origins: [`chrome-extension://${EXT}/`] }); const ff = JSON.parse(readFileSync(join(e.home, "Library/Application Support/Mozilla/NativeMessagingHosts/org.coldai.clipwallet.link.json"), "utf8")); expect(ff.allowed_extensions).toEqual(["wallet@clipwallet.coldai.org"]); - expect(statSync(launcherPath(e)).mode & 0o777).toBe(0o700); + // POSIX permission bits (Windows has none: Node reports 0o666 for any writable file there). + if (process.platform !== "win32") expect(statSync(launcherPath(e)).mode & 0o777).toBe(0o700); expect(readFileSync(launcherPath(e), "utf8")).toContain("ELECTRON_RUN_AS_NODE=1"); // Someone else's manifest with the same name (another install) is left alone. const brave = join(e.home, "Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts/org.coldai.clipwallet.link.json"); diff --git a/packages/desktop-kit/test/url-policy.test.ts b/packages/desktop-kit/test/url-policy.test.ts index 7605f9d..de33d70 100644 --- a/packages/desktop-kit/test/url-policy.test.ts +++ b/packages/desktop-kit/test/url-policy.test.ts @@ -1,3 +1,4 @@ +import { join, sep } from "node:path"; import { describe, expect, it } from "vitest"; import { displayHost, isNavigable, normalizeInput, partitionFor, promptFor, webOrigin } from "../src/main/browser/url-policy"; import { resolveAppPath, walletCsp } from "../src/main/app-paths"; @@ -38,12 +39,14 @@ describe("url policy", () => { describe("clip-app protocol", () => { it("serves only files inside the bundle", () => { - expect(resolveAppPath("/app/out/renderer", "clip-app://wallet/wallet/index.html")).toBe("/app/out/renderer/wallet/index.html"); - expect(resolveAppPath("/app/out/renderer", "clip-app://wallet/../../../etc/passwd")).toBe("/app/out/renderer/etc/passwd"); - expect(resolveAppPath("/app/out/renderer", "clip-app://wallet/..%2F..%2Fsecret")).toBeNull(); - expect(resolveAppPath("/app/out/renderer", "clip-app://wallet/%2e%2e/%2e%2e/secret")).toBe("/app/out/renderer/secret"); - expect(resolveAppPath("/app/out/renderer", "clip-app://other/wallet/index.html")).toBeNull(); - expect(resolveAppPath("/app/out/renderer", "https://wallet/index.html")).toBeNull(); + // In the platform's own path form, as the app passes it (backslashes on Windows). + const root = join(sep, "app", "out", "renderer"); + expect(resolveAppPath(root, "clip-app://wallet/wallet/index.html")).toBe(join(root, "wallet", "index.html")); + expect(resolveAppPath(root, "clip-app://wallet/../../../etc/passwd")).toBe(join(root, "etc", "passwd")); + expect(resolveAppPath(root, "clip-app://wallet/..%2F..%2Fsecret")).toBeNull(); + expect(resolveAppPath(root, "clip-app://wallet/%2e%2e/%2e%2e/secret")).toBe(join(root, "secret")); + expect(resolveAppPath(root, "clip-app://other/wallet/index.html")).toBeNull(); + expect(resolveAppPath(root, "https://wallet/index.html")).toBeNull(); }); it("CSP: no remote script, no eval, no frames; images only from the media proxy", () => { const csp = walletCsp("https://media.example/v1"); From 6357c22b9e9c2fc41d2552083c162e857f2843d8 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:57:04 +0800 Subject: [PATCH 6/6] fix(ci): Windows runs the POSIX-style scripts; repro-check's second run can be read back - pnpm-workspace.yaml: shellEmulator, so scripts that set variables POSIX-style (NODE_OPTIONS=--conditions=development electron-vite build, the desktop app's dist:*, the extension's build) run the same way under cmd.exe; the desktop workflow's Windows build stopped at "'NODE_OPTIONS' is not recognized". - scripts/repro-check.sh: the container writes the checksums as root under umask 077; it now makes them readable, so the host can compare the two runs. Co-Authored-By: Claude Opus 5.5 --- pnpm-workspace.yaml | 3 +++ scripts/repro-check.sh | 2 ++ 2 files changed, 5 insertions(+) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6616c83..890400f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,6 +6,9 @@ packages: - services/* # Stock wallet-picker UIs for the extension's picker e2e (own peer versions: wagmi 2, React 18). Private. - apps/extension/e2e/pickers/dapps +# Scripts set variables POSIX-style (NODE_OPTIONS=--conditions=development …); the emulator runs them the same way on +# Windows (cmd.exe has no VAR=value prefix), so the desktop app builds and packages there too. +shellEmulator: true allowBuilds: '@parcel/watcher': false esbuild: true diff --git a/scripts/repro-check.sh b/scripts/repro-check.sh index 5f5d14e..53f4e89 100755 --- a/scripts/repro-check.sh +++ b/scripts/repro-check.sh @@ -47,6 +47,8 @@ run() { pnpm install --frozen-lockfile --reporter=silent --filter \"@clip-wallet/extension...\" pnpm --filter @clip-wallet/extension package >/out/package.log 2>&1 || { tail -50 /out/package.log; exit 1; } cp apps/extension/release/SHA256SUMS apps/extension/release/TREE-DIGESTS /out/ + # The container runs as root; with umask 077 the host user couldn't read what it wrote. + chmod a+r /out/* " echo "run $n ($tz, $lang, umask $mask):" sed 's/^/ /' "$WORK/out$n/SHA256SUMS"