5cf1338dfd8b
camera Mayan scaffold: @capacitor/camera 8.2.0, MayanRestoreView replaces ComingSoon with Scan-with-camera (Android) + Choose-from-gallery; handleMayanDecode stub returns typed result for decoder swap-in
- time
- 2026-05-01 04:30 UTC · 5 months ago
- machine
- koh2
- parent
- 96bcd2845cab
↓ peach-browser-5cf1338d.tar.gzatlas at this save
files changed6 files
android/app/capacitor.build.gradle+1 −0android/app/src/main/assets/capacitor.plugins.json+4 −0android/capacitor.settings.gradle+9 −6package.json+1 −0pnpm-lock.yaml+158 −0src/popup/views/Lock.tsx+108 −20android/app/capacitor.build.gradle+1 −0
@@ -10,6 +10,7 @@1010 apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"1111 dependencies {1212 implementation project(':capacitor-app')13+ implementation project(':capacitor-camera')1314 implementation project(':capacitor-haptics')1415 implementation project(':capacitor-keyboard')1516 implementation project(':capacitor-preferences')android/app/src/main/assets/capacitor.plugins.json+4 −0
@@ -4,6 +4,10 @@44 "classpath": "com.capacitorjs.plugins.app.AppPlugin"55 },66 {7+ "pkg": "@capacitor/camera",8+ "classpath": "com.capacitorjs.plugins.camera.CameraPlugin"9+ },10+ {711 "pkg": "@capacitor/haptics",812 "classpath": "com.capacitorjs.plugins.haptics.HapticsPlugin"913 },android/capacitor.settings.gradle+9 −6
@@ -1,18 +1,21 @@11 // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN22 include ':capacitor-android'3-project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')3+project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@capacitor/android/capacitor')44 55 include ':capacitor-app'6-project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')6+project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@capacitor/app/android')77 8+include ':capacitor-camera'9+project(':capacitor-camera').projectDir = new File('../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@capacitor/camera/android')10+811 include ':capacitor-haptics'9-project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android')12+project(':capacitor-haptics').projectDir = new File('../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@capacitor/haptics/android')1013 1114 include ':capacitor-keyboard'12-project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android')15+project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@capacitor/keyboard/android')1316 1417 include ':capacitor-preferences'15-project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android')18+project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@capacitor/preferences/android')1619 1720 include ':capacitor-status-bar'18-project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')21+project(':capacitor-status-bar').projectDir = new File('../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@capacitor/status-bar/android')package.json+1 −0
@@ -22,6 +22,7 @@2222 "@bokuweb/zstd-wasm": "^0.0.27",2323 "@capacitor/android": "^8.3.1",2424 "@capacitor/app": "^8.1.0",25+ "@capacitor/camera": "^8.2.0",2526 "@capacitor/cli": "^8.3.1",2627 "@capacitor/core": "^8.3.1",2728 "@capacitor/haptics": "^8.0.2",pnpm-lock.yaml+158 −0
@@ -17,6 +17,9 @@1717 '@capacitor/app':1818 specifier: ^8.1.01919 version: 8.1.0(@capacitor/[email protected])20+ '@capacitor/camera':21+ specifier: ^8.2.022+ version: 8.2.0(@capacitor/[email protected])2023 '@capacitor/cli':2124 specifier: ^8.3.12225 version: 8.3.1@@ -44,6 +47,27 @@4447 '@noble/hashes':4548 specifier: ^2.2.04649 version: 2.2.050+ '@stablelib/blake2s':51+ specifier: ^2.0.152+ version: 2.0.153+ '@stablelib/chacha20poly1305':54+ specifier: ^2.0.155+ version: 2.0.156+ '@stablelib/hkdf':57+ specifier: ^2.0.158+ version: 2.0.159+ '@stablelib/hmac':60+ specifier: ^2.0.161+ version: 2.0.162+ '@stablelib/random':63+ specifier: ^2.0.164+ version: 2.0.165+ '@stablelib/sha256':66+ specifier: ^2.0.167+ version: 2.0.168+ '@stablelib/x25519':69+ specifier: ^2.0.170+ version: 2.0.14771 eff-diceware-passphrase:4872 specifier: ^3.0.04973 version: 3.0.0@@ -234,6 +258,11 @@234258 peerDependencies:235259 '@capacitor/core': '>=8.0.0'236260 261+ '@capacitor/[email protected]':262+ resolution: {integrity: sha512-hYfrT6xpL936qoEkIpJzSnb0fQCaTkOux1cXzGBfH8QLOGqr6gSLiWZlZz/fqMPmMKJMNRBqlTQkj5fuMhVZog==}263+ peerDependencies:264+ '@capacitor/core': '>=8.0.0'265+237266 '@capacitor/[email protected]':238267 resolution: {integrity: sha512-1sPGW4THTDfR6YjXwZ0jM7oAfAtciPOHN00qs/3sNAQx1kKrrEYSfDPwCm1/xlAgi0OeL69SiRfw314Ans+1sw==}239268 engines: {node: '>=22.0.0'}@@ -607,6 +636,57 @@607636 resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}608637 engines: {node: '>= 8.0.0'}609638 639+ '@stablelib/[email protected]':640+ resolution: {integrity: sha512-U/RMANRxbT/ahIpYsPSiFwDFNjADHdnCFfmo09MO1ai2XmerPAOPtMl0qmX7XVvygnACC6ijKDyHBoT2rGyElg==}641+642+ '@stablelib/[email protected]':643+ resolution: {integrity: sha512-U9iAO8lXgEDONsA0zPPSgcf3HUBNAqHiJmSHgZz62OvC3Hi2Bhc5kTnQ3S1/L+sthDTHtCMhcEiklmIly6uQ3w==}644+645+ '@stablelib/[email protected]':646+ resolution: {integrity: sha512-tEIp6yEq/3Ue7D21xy8+zhv/6G3gruZOCctgtVeVYCuqRDFWFRIDZH+8Pb0f4Wk/VwfkDlG1V4yaB6WjkzrdRw==}647+648+ '@stablelib/[email protected]':649+ resolution: {integrity: sha512-QIzI6V7nkJA5CjOZ7GoceBd4CIKrJoC471VaI6jh1xPQ2cMhkhQK4HddyzCXOR2y+fBF3/5B2HO3FXXI9C+Xzg==}650+651+ '@stablelib/[email protected]':652+ resolution: {integrity: sha512-kOoBsXbDPVRlelzXl+5WViycgM19lD7lF3Bc3KWI+DzId0Stc2HlxAfSc+Xpn3RgqSCl1ZNTXr33LegqBhBBaw==}653+654+ '@stablelib/[email protected]':655+ resolution: {integrity: sha512-lS1FqtNqofxe2vLkRsLli2m3x/XanUyAYRphLhdHumKeIsLbjbCXdCq3Pf/eWiO7G3QlSG5ViqnoVjktzfLWMg==}656+657+ '@stablelib/[email protected]':658+ resolution: {integrity: sha512-0NWPogffRm+UWBH0+iM5otZmNrVe5OHFIvyoNIVankMAYOQzMwcdVALOVPrB5Ho0dST+Oc3H8/hPh65Z8R/uew==}659+660+ '@stablelib/[email protected]':661+ resolution: {integrity: sha512-u3WPSqGido8lwJuMcrBgM5K54LrPGhkWAdtsyccf7dGsLixAZUds77zOAbu7bvKPwQlmoByH0txBi5rTmEKuHg==}662+663+ '@stablelib/[email protected]':664+ resolution: {integrity: sha512-R8w5r43QpM92FhyGPsQAhd4KOKwhmMFgvihoRJjiEi/YNwwXlluQoYruNQxyBA1fEMmJ8QHtQvEZEX/Uyr6MwQ==}665+666+ '@stablelib/[email protected]':667+ resolution: {integrity: sha512-YDMEmogYNDWESU8ggMiTi4VDMfHt2XG+pgVskJCHuKVlmWwXxYKZ6bn5dq24/jU3vzZSajwKzRCU6iafT2OKUA==}668+669+ '@stablelib/[email protected]':670+ resolution: {integrity: sha512-Ht63fQp3wz/F8U4AlXEPb7hfJOIILs8Lq55jgtD7KueWtyjhVuzcsGLSTAWtZs3XJDZYdF1WcSKn+kBtbzupww==}671+672+ '@stablelib/[email protected]':673+ resolution: {integrity: sha512-2+tWBLCMtWlHQ7GqjD5L+lQRyWtun4Lou0IOdTML8zuTuAS0EgihnHFx+4uMZwYU1In40J/WlpyKSLidHfStRQ==}674+675+ '@stablelib/[email protected]':676+ resolution: {integrity: sha512-D8xfZcL/5zeVARJ9I2fZOUCnZvsJx8G4JDQvD+ecsnaCildPtqZDwqqjtOc4cfHAdScrMjOzUVsssjXUTBw3YQ==}677+678+ '@stablelib/[email protected]':679+ resolution: {integrity: sha512-W6GAtXEEs7r+dSbuBsvoFmlyL3gLxle41tQkjKu17dDWtDdjhVUbtRfRCQcCUeczwkgjQxMPopgwYEvxXtHXGw==}680+681+ '@stablelib/[email protected]':682+ resolution: {integrity: sha512-LA1PaLDc6Lv72ppA4PEZ7abDE741KfG7k7QhBiUyIfViMqrwWv8HqQQFPeuPfS4k2OxFv++IAgc8HlvdBatD+w==}683+684+ '@stablelib/[email protected]':685+ resolution: {integrity: sha512-1eU2K9EgOcV4qc9jcP6G72xxZxEm5PfeI5H55l08W95b4oRJaqhmlWRc4xZAm6IVSKhVNxMi66V67hCzzuMTAg==}686+687+ '@stablelib/[email protected]':688+ resolution: {integrity: sha512-qi04HS2puHaBf50kM/kes5QcZFGsx8yF0YmCjLCOa/LPmnBaKEKX9ZR82OnnCwMn72YH13R/bBZgr/UP0aPFfA==}689+610690 '@standard-schema/[email protected]':611691 resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}612692 @@ -2818,6 +2898,10 @@28182898 dependencies:28192899 '@capacitor/core': 8.3.128202900 2901+ '@capacitor/[email protected](@capacitor/[email protected])':2902+ dependencies:2903+ '@capacitor/core': 8.3.12904+28212905 '@capacitor/[email protected]':28222906 dependencies:28232907 '@ionic/cli-framework-output': 2.2.8@@ -3207,6 +3291,80 @@32073291 dependencies:32083292 estree-walker: 2.0.232093293 picomatch: 2.3.23294+3295+ '@stablelib/[email protected]': {}3296+3297+ '@stablelib/[email protected]':3298+ dependencies:3299+ '@stablelib/int': 2.0.13300+3301+ '@stablelib/[email protected]':3302+ dependencies:3303+ '@stablelib/binary': 2.0.13304+ '@stablelib/hash': 2.0.03305+ '@stablelib/wipe': 2.0.13306+3307+ '@stablelib/[email protected]': {}3308+3309+ '@stablelib/[email protected]':3310+ dependencies:3311+ '@stablelib/aead': 2.0.03312+ '@stablelib/binary': 2.0.13313+ '@stablelib/chacha': 2.0.13314+ '@stablelib/constant-time': 2.0.13315+ '@stablelib/poly1305': 2.0.13316+ '@stablelib/wipe': 2.0.13317+3318+ '@stablelib/[email protected]':3319+ dependencies:3320+ '@stablelib/binary': 2.0.13321+ '@stablelib/wipe': 2.0.13322+3323+ '@stablelib/[email protected]': {}3324+3325+ '@stablelib/[email protected]': {}3326+3327+ '@stablelib/[email protected]':3328+ dependencies:3329+ '@stablelib/hash': 2.0.03330+ '@stablelib/hmac': 2.0.13331+ '@stablelib/wipe': 2.0.13332+3333+ '@stablelib/[email protected]':3334+ dependencies:3335+ '@stablelib/constant-time': 2.0.13336+ '@stablelib/hash': 2.0.03337+ '@stablelib/wipe': 2.0.13338+3339+ '@stablelib/[email protected]': {}3340+3341+ '@stablelib/[email protected]':3342+ dependencies:3343+ '@stablelib/bytes': 2.0.13344+3345+ '@stablelib/[email protected]':3346+ dependencies:3347+ '@stablelib/constant-time': 2.0.13348+ '@stablelib/wipe': 2.0.13349+3350+ '@stablelib/[email protected]':3351+ dependencies:3352+ '@stablelib/binary': 2.0.13353+ '@stablelib/wipe': 2.0.13354+3355+ '@stablelib/[email protected]':3356+ dependencies:3357+ '@stablelib/binary': 2.0.13358+ '@stablelib/hash': 2.0.03359+ '@stablelib/wipe': 2.0.13360+3361+ '@stablelib/[email protected]': {}3362+3363+ '@stablelib/[email protected]':3364+ dependencies:3365+ '@stablelib/keyagreement': 2.0.13366+ '@stablelib/random': 2.0.13367+ '@stablelib/wipe': 2.0.132103368 32113369 '@standard-schema/[email protected]': {}32123370 src/popup/views/Lock.tsx+108 −20
@@ -160,7 +160,7 @@160160 161161 {/* ── Mayan code restore (decoder coming soon) ──────────────── */}162162 <Match when={restoreSurface() === "mayan"}>163- <MayanComingSoonView onBack={() => setRestoreSurface("restore")} />163+ <MayanRestoreView onBack={() => setRestoreSurface("restore")} />164164 </Match>165165 166166 {/* ── PeachScript restore ───────────────────────────────────── */}@@ -459,11 +459,10 @@459459 role="listitem"460460 onClick={props.onPickMayan}461461 >462- <span class="peach-restore-card-soon" aria-label="coming soon">soon</span>463462 <h2 class="peach-restore-card-headline">Mayan code</h2>464463 <hr class="peach-restore-card-divider" />465464 <p class="peach-restore-card-body">466- Restore from a printed Mayan code by uploading a photo of it.465+ Restore from a printed Mayan code — scan or upload a photo.467466 </p>468467 <p class="peach-restore-card-meta">decoder shipping soon</p>469468 </button>@@ -487,9 +486,75 @@487486 );488487 }489488 490-// ── Mayan code (decoder coming soon) ───────────────────────────────────────489+// ── Mayan code restore ─────────────────────────────────────────────────────491490 492-function MayanComingSoonView(props: { onBack: () => void }) {491+/**492+ * Decode a base64-encoded Mayan code image into the binary backup payload.493+ * Decoder isn't shipped yet — return a typed result so the UI can surface494+ * the right copy, and replace this single function when the decoder lands.495+ */496+async function handleMayanDecode(_base64: string): Promise<497+ { ok: true; payload: Uint8Array } | { ok: false; reason: string }498+> {499+ // TODO(decoder): wire src/crypto/mayan.ts decode() here.500+ return { ok: false, reason: "Mayan decoder is not yet shipped." };501+}502+503+function MayanRestoreView(props: { onBack: () => void }) {504+ const [decodeError, setDecodeError] = createSignal<string | null>(null);505+ const [decoding, setDecoding] = createSignal(false);506+ const isAndroid = createMemo(() =>507+ Capacitor.isNativePlatform() && Capacitor.getPlatform() === "android",508+ );509+510+ async function processBase64(base64: string) {511+ setDecoding(true);512+ setDecodeError(null);513+ try {514+ const result = await handleMayanDecode(base64);515+ if (!result.ok) {516+ setDecodeError(result.reason);517+ return;518+ }519+ // Future: hand payload to the restore-decrypt flow here.520+ setDecodeError("Decoded — but the restore flow isn't wired yet.");521+ } finally {522+ setDecoding(false);523+ }524+ }525+526+ async function handleCameraScan() {527+ setDecodeError(null);528+ try {529+ const { Camera, CameraResultType, CameraSource } = await import("@capacitor/camera");530+ const image = await Camera.getPhoto({531+ quality: 90,532+ resultType: CameraResultType.Base64,533+ source: CameraSource.Camera,534+ allowEditing: false,535+ });536+ if (image.base64String) await processBase64(image.base64String);537+ } catch (err) {538+ // User cancellation throws — only surface real errors.539+ const msg = err instanceof Error ? err.message : "";540+ if (msg && !/cancel/i.test(msg)) setDecodeError(msg);541+ }542+ }543+544+ function handleFileUpload(e: Event) {545+ const input = e.currentTarget as HTMLInputElement;546+ const file = input.files?.[0];547+ if (!file) return;548+ const reader = new FileReader();549+ reader.onload = () => {550+ const result = reader.result;551+ if (typeof result !== "string") return;552+ const base64 = result.split(",", 2)[1] ?? "";553+ void processBase64(base64);554+ };555+ reader.readAsDataURL(file);556+ }557+493558 return (494559 <section495560 class="peach-screen peach-theme lock-screen lock-screen--vault-stage lock-screen--onboarding peach-restore-screen"@@ -504,27 +569,50 @@504569 505570 <div class="peach-lock-hero">506571 <span class="peach-lock-eyebrow">mayan code</span>507- <h1 class="peach-onboarding-headline">508- decoder509- <br />510- on its way.511- </h1>572+ <h1 class="peach-onboarding-headline">scan or upload.</h1>512573 </div>513574 514575 <div class="peach-restore-stack">515- <div class="peach-restore-card peach-restore-card--static">516- <h2 class="peach-restore-card-headline">Not yet — but soon.</h2>576+ <Show when={isAndroid()}>577+ <button578+ class="peach-restore-card"579+ disabled={decoding()}580+ onClick={() => void handleCameraScan()}581+ >582+ <h2 class="peach-restore-card-headline">Scan with camera</h2>583+ <hr class="peach-restore-card-divider" />584+ <p class="peach-restore-card-body">585+ Point your camera at the printed Mayan code.586+ </p>587+ </button>588+ </Show>589+590+ <label591+ class="peach-restore-card"592+ classList={{ "peach-restore-card--disabled": decoding() }}593+ >594+ <h2 class="peach-restore-card-headline">Choose from gallery</h2>517595 <hr class="peach-restore-card-divider" />518596 <p class="peach-restore-card-body">519- Mayan codes encode the same backup as PeachScript, but printable on520- a single page. The phone-camera decoder is the next thing to land521- for this format.597+ Pick a saved photo of your Mayan code.522598 </p>523- <p class="peach-restore-card-body">524- Until then, restore your vault using the PeachScript that came525- with this Mayan code, or open a <code>.peach</code> file backup.526- </p>527- </div>599+ <input600+ type="file"601+ accept="image/*"602+ style="display:none"603+ onChange={handleFileUpload}604+ disabled={decoding()}605+ />606+ </label>607+608+ <Show when={decodeError()}>609+ <div class="peach-restore-card peach-restore-card--static">610+ <p class="peach-restore-card-body">{decodeError()}</p>611+ <p class="peach-restore-card-meta">612+ Until the decoder lands, restore via PeachScript or a <code>.peach</code> file.613+ </p>614+ </div>615+ </Show>528616 </div>529617 530618 <div class="peach-restore-footer">