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
6 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 −20
android/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')

view file

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 	},

view file

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')

view file

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",

view file

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 

view file

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">

view file