From 373e7b015c46844da3af768cf327885f968808ad Mon Sep 17 00:00:00 2001 From: IMT <1llum1n4t1@duck.com> Date: Tue, 23 Jun 2026 20:45:10 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(mobile):=20Capacitor=208=20=E5=8C=96?= =?UTF-8?q?=EF=BC=8B=E8=B2=B7=E3=81=84=E5=88=87=E3=82=8AIAP=E3=82=92=20@ca?= =?UTF-8?q?pgo/native-purchases=20=E3=81=A7=E5=AE=9F=E9=85=8D=E7=B7=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Capacitor 6→8 アップグレード(@capacitor/* 8.4.1・app8.1/preferences8.0)。保守される IAP プラグインが Cap8 必須のため。mobile はネイティブ未ビルド=アップグレードの好機。 - iap.js: @capgo/native-purchases(StoreKit2/Play Billing 直叩き・外部SaaS不要)で initIap/purchase/restore を実配線。所有判定は iOS=currentEntitlements・Android= purchaseState"1"&acknowledged。ストア照会結果を Preferences にキャッシュ(オフライン即時表示)。 web(dev)は従来の localStorage 解錠(本番は import.meta.env.DEV で dead code=バイパス防止)。 - main.js: onBuy を未解錠時に「解禁」と誤表示しないよう堅牢化。QRカメラのコメントを 「iOS 14.3+ で getUserMedia 動作・barcode-scanner 差し替え不要」と確認した内容へ更新。 - README: Cap8/IAP配線/CI を実態に更新。 回帰: _sync_repro 223 / _mobile_crud_repro 15 / _mobile_sync_repro 9(実relay e2e)緑。 Co-Authored-By: Claude Opus 4.8 --- mobile/README.md | 16 +- mobile/package.json | 13 +- mobile/pnpm-lock.yaml | 378 +++++++++++++++++++----------------------- mobile/src/iap.js | 119 +++++++++++-- mobile/src/main.js | 11 +- 5 files changed, 299 insertions(+), 238 deletions(-) diff --git a/mobile/README.md b/mobile/README.md index 5eefd2c..c87d538 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -11,7 +11,7 @@ PC 拡張の同期エンジンをそのまま再利用し、**クラウド同期 - `src/storage-shim.js` … `chrome.storage.local` / `onChanged` を 1 プロセス KV で再現(バックエンド注入式) - `src/preferences-backend.js` … シムの裏付け(Capacitor Preferences) - `src/sync-orchestrator.js` … 拡張 `background.js` のモバイル版(reconcile スケジューリング+realtime WS) -- `src/iap.js` … 買い切り課金(解禁ゲート。ネイティブ配線は TODO) +- `src/iap.js` … 買い切り課金の解禁ゲート(`@capgo/native-purchases` で StoreKit2 / Play Billing を実配線。所有判定はストア照会+Preferences キャッシュ。web は dev 解錠フラグ) - `vite.config.js` … `@shared` → `../src/shared`(同期エンジンを**単一ソース**で参照) 同期エンジン(`vault.js`/`sync.js`/`relay-transport.js`/`storage.js`/`markdown.js`)はコピーせず拡張と共有する。 @@ -39,12 +39,14 @@ pnpm -C mobile exec cap sync ## ビルド(CI) -- Android: [`.github/workflows/mobile-android.yml`](../.github/workflows/mobile-android.yml)(ubuntu・JDK17・Gradle)。署名は未設定=当面 debug APK。 -- iOS: macOS runner + 署名証明書(App Store Connect API key / provisioning)を Secrets に投入してから有効化する(TODO)。 +Capacitor 8(Android: minSdk24 / compileSdk36 / Gradle 8.14.3 / **JDK21**、iOS: deployment target 15 / **Xcode26+** / SPM)。 + +- Android: [`.github/workflows/mobile-android.yml`](../.github/workflows/mobile-android.yml)(ubuntu・JDK21)。`cap add android`→CAMERA/BILLING 権限を AndroidManifest へ注入→`assembleDebug`→APK artifact。署名は未設定=当面 debug APK(release 署名は keystore を Secrets 投入後に追加)。 +- iOS: [`.github/workflows/mobile-ios.yml`](../.github/workflows/mobile-ios.yml)(macOS runner・SPM)。`cap add ios`→Info.plist へ NSCameraUsageDescription 注入→署名なし simulator コンパイル検証。実機 .ipa/署名/申請は Apple Developer 証明書を Secrets 投入後に追加。 ## 残 TODO -- IAP プラグイン配線(`src/iap.js`:product id `jp.nephilim.petarin.sync` を App Store / Play に non-consumable で登録 → restore/purchase)。 -- 付箋の新規作成/編集 UI(現状は同期表示が主・閲覧中心)。 -- 本番 relay は Custom Domain へ(拡張側と共通課題)。 -- iOS 署名と CI、ストア申請(`/vava` 連携)。 +- ストア側 product 登録: App Store Connect / Google Play で `jp.nephilim.petarin.sync` を non-consumable(¥500)登録(ストア側手作業)。 +- ネイティブ実機の目視確認(iPhone/Android で付箋 CRUD・ペアリング・カメラ QR・購入フロー)。 +- iOS 署名証明書(App Store Connect API key / provisioning)を Secrets 投入して署名ビルド/ストア申請(`/vava` 連携)。 +- 課金 enforcement の強化(当面はクライアント側のストア所有判定=`iap.js`、後段で relay 側 enforcement へ)。 diff --git a/mobile/package.json b/mobile/package.json index fe35962..7430097 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -14,16 +14,17 @@ "open:ios": "cap open ios" }, "dependencies": { - "@capacitor/android": "^6.1.0", - "@capacitor/app": "^6.0.1", - "@capacitor/core": "^6.1.0", - "@capacitor/ios": "^6.1.0", - "@capacitor/preferences": "^6.0.1", + "@capacitor/android": "^8.4.1", + "@capacitor/app": "^8.1.0", + "@capacitor/core": "^8.4.1", + "@capacitor/ios": "^8.4.1", + "@capacitor/preferences": "^8.0.1", + "@capgo/native-purchases": "^8.4.6", "jsqr": "^1.4.0", "qrcode-generator": "^2.0.4" }, "devDependencies": { - "@capacitor/cli": "^6.1.0", + "@capacitor/cli": "^8.4.1", "@vitejs/plugin-basic-ssl": "1.2.0", "vite": "^5.4.0" } diff --git a/mobile/pnpm-lock.yaml b/mobile/pnpm-lock.yaml index e317e9e..929c75d 100644 --- a/mobile/pnpm-lock.yaml +++ b/mobile/pnpm-lock.yaml @@ -9,20 +9,23 @@ importers: .: dependencies: '@capacitor/android': - specifier: ^6.1.0 - version: 6.2.1(@capacitor/core@6.2.1) + specifier: ^8.4.1 + version: 8.4.1(@capacitor/core@8.4.1) '@capacitor/app': - specifier: ^6.0.1 - version: 6.0.3(@capacitor/core@6.2.1) + specifier: ^8.1.0 + version: 8.1.0(@capacitor/core@8.4.1) '@capacitor/core': - specifier: ^6.1.0 - version: 6.2.1 + specifier: ^8.4.1 + version: 8.4.1 '@capacitor/ios': - specifier: ^6.1.0 - version: 6.2.1(@capacitor/core@6.2.1) + specifier: ^8.4.1 + version: 8.4.1(@capacitor/core@8.4.1) '@capacitor/preferences': - specifier: ^6.0.1 - version: 6.0.4(@capacitor/core@6.2.1) + specifier: ^8.0.1 + version: 8.0.1(@capacitor/core@8.4.1) + '@capgo/native-purchases': + specifier: ^8.4.6 + version: 8.4.6(@capacitor/core@8.4.1) jsqr: specifier: ^1.4.0 version: 1.4.0 @@ -31,8 +34,8 @@ importers: version: 2.0.4 devDependencies: '@capacitor/cli': - specifier: ^6.1.0 - version: 6.2.1 + specifier: ^8.4.1 + version: 8.4.1 '@vitejs/plugin-basic-ssl': specifier: 1.2.0 version: 1.2.0(vite@5.4.21(@types/node@26.0.0)) @@ -42,33 +45,38 @@ importers: packages: - '@capacitor/android@6.2.1': - resolution: {integrity: sha512-8gd4CIiQO5LAIlPIfd5mCuodBRxMMdZZEdj8qG8m+dQ1sQ2xyemVpzHmRK8qSCHorsBUCg3D62j2cp6bEBAkdw==} + '@capacitor/android@8.4.1': + resolution: {integrity: sha512-igtDCJ7QQn0P2qHFD9p4KXaa6V1b2PRNt+MxjVwtjTm/BJvqmiazOJq6rPjwFSZnfHm6iFoZk8TfzHd44pyBGw==} peerDependencies: - '@capacitor/core': ^6.2.0 + '@capacitor/core': ^8.4.0 - '@capacitor/app@6.0.3': - resolution: {integrity: sha512-4gFUCbcVz0N/YYN32OBFerocWXslIv3Nc90gDiRsBkJc0plwK6kIUT6PKa5WtW2kfhteUeCVXQbvArH2fH+0Ug==} + '@capacitor/app@8.1.0': + resolution: {integrity: sha512-MlmttTOWHDedr/G4SrhNRxsXMqY+R75S4MM4eIgzsgCzOYhb/MpCkA5Q3nuOCfL1oHm26xjUzqZ5aupbOwdfYg==} peerDependencies: - '@capacitor/core': ^6.0.0 + '@capacitor/core': '>=8.0.0' - '@capacitor/cli@6.2.1': - resolution: {integrity: sha512-JKl0FpFge8PgQNInw12kcKieQ4BmOyazQ4JGJOfEpVXlgrX1yPhSZTPjngupzTCiK3I7q7iGG5kjun0fDqgSCA==} - engines: {node: '>=18.0.0'} + '@capacitor/cli@8.4.1': + resolution: {integrity: sha512-t7F2s7fFHCq113xgrggrmK6ctV0/8E5YfLNVLfPHp4GCTDO+tly9fZvWPf2/sOI8lMm18dLT43qbXLRTz/OZgw==} + engines: {node: '>=22.0.0'} hasBin: true - '@capacitor/core@6.2.1': - resolution: {integrity: sha512-urZwxa7hVE/BnA18oCFAdizXPse6fCKanQyEqpmz6cBJ2vObwMpyJDG5jBeoSsgocS9+Ax+9vb4ducWJn0y2qQ==} + '@capacitor/core@8.4.1': + resolution: {integrity: sha512-xqhOGLbTAYeOWK+IDUNSjQJAPapQjRHrIcgk9PYp52or9zFTaaMko31uNi16N6W+CRJ8VrRram6fOYILkBG2Hg==} + + '@capacitor/ios@8.4.1': + resolution: {integrity: sha512-EgcAk7NYheHMTyP3CUrA65qKs4D2UEAKgw44HI6Uk3dTw6KjLQkdLOQWvbeRncHaZ2gklfOojUoc5DlSw2lhYg==} + peerDependencies: + '@capacitor/core': ^8.4.0 - '@capacitor/ios@6.2.1': - resolution: {integrity: sha512-tbMlQdQjxe1wyaBvYVU1yTojKJjgluZQsJkALuJxv/6F8QTw5b6vd7X785O/O7cMpIAZfUWo/vtAHzFkRV+kXw==} + '@capacitor/preferences@8.0.1': + resolution: {integrity: sha512-T6no3ebi79XJCk91U3Jp/liJUwgBdvHR+s6vhvPkPxSuch7z3zx5Rv1bdWM6sWruNx+pViuEGqZvbfCdyBvcHQ==} peerDependencies: - '@capacitor/core': ^6.2.0 + '@capacitor/core': '>=8.0.0' - '@capacitor/preferences@6.0.4': - resolution: {integrity: sha512-ziauSI1pgdyl+gduvvf8lInvzF3Wdyu/ok+u7NlnhKp8XOj9plJgtnXZWFiR8CiCK5wMvo+gFaNh3zhMyEUwpA==} + '@capgo/native-purchases@8.4.6': + resolution: {integrity: sha512-LkiiTjLh+IVqXc6qYztkiNMwnHhIn2DCl8q0qp84B8zq/Mf5gDXI3C5XVo1qyJzbv2drOwNaHxOtPT0A08Vajg==} peerDependencies: - '@capacitor/core': ^6.0.0 + '@capacitor/core': '>=8.0.0' '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} @@ -212,42 +220,38 @@ packages: resolution: {integrity: sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==} engines: {node: '>=16.0.0'} - '@ionic/utils-array@2.1.5': - resolution: {integrity: sha512-HD72a71IQVBmQckDwmA8RxNVMTbxnaLbgFOl+dO5tbvW9CkkSFCv41h6fUuNsSEVgngfkn0i98HDuZC8mk+lTA==} - engines: {node: '>=10.3.0'} - - '@ionic/utils-fs@3.1.6': - resolution: {integrity: sha512-eikrNkK89CfGPmexjTfSWl4EYqsPSBh0Ka7by4F0PLc1hJZYtJxUZV3X4r5ecA8ikjicUmcbU7zJmAjmqutG/w==} - engines: {node: '>=10.3.0'} + '@ionic/utils-array@2.1.6': + resolution: {integrity: sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==} + engines: {node: '>=16.0.0'} '@ionic/utils-fs@3.1.7': resolution: {integrity: sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==} engines: {node: '>=16.0.0'} - '@ionic/utils-object@2.1.5': - resolution: {integrity: sha512-XnYNSwfewUqxq+yjER1hxTKggftpNjFLJH0s37jcrNDwbzmbpFTQTVAp4ikNK4rd9DOebX/jbeZb8jfD86IYxw==} - engines: {node: '>=10.3.0'} - - '@ionic/utils-process@2.1.10': - resolution: {integrity: sha512-mZ7JEowcuGQK+SKsJXi0liYTcXd2bNMR3nE0CyTROpMECUpJeAvvaBaPGZf5ERQUPeWBVuwqAqjUmIdxhz5bxw==} - engines: {node: '>=10.3.0'} + '@ionic/utils-object@2.1.6': + resolution: {integrity: sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==} + engines: {node: '>=16.0.0'} - '@ionic/utils-stream@3.1.5': - resolution: {integrity: sha512-hkm46uHvEC05X/8PHgdJi4l4zv9VQDELZTM+Kz69odtO9zZYfnt8DkfXHJqJ+PxmtiE5mk/ehJWLnn/XAczTUw==} - engines: {node: '>=10.3.0'} + '@ionic/utils-process@2.1.12': + resolution: {integrity: sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==} + engines: {node: '>=16.0.0'} - '@ionic/utils-subprocess@2.1.11': - resolution: {integrity: sha512-6zCDixNmZCbMCy5np8klSxOZF85kuDyzZSTTQKQP90ZtYNCcPYmuFSzaqDwApJT4r5L3MY3JrqK1gLkc6xiUPw==} - engines: {node: '>=10.3.0'} + '@ionic/utils-stream@3.1.7': + resolution: {integrity: sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==} + engines: {node: '>=16.0.0'} - '@ionic/utils-terminal@2.3.3': - resolution: {integrity: sha512-RnuSfNZ5fLEyX3R5mtcMY97cGD1A0NVBbarsSQ6yMMfRJ5YHU7hHVyUfvZeClbqkBC/pAqI/rYJuXKCT9YeMCQ==} - engines: {node: '>=10.3.0'} + '@ionic/utils-subprocess@3.0.1': + resolution: {integrity: sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==} + engines: {node: '>=16.0.0'} '@ionic/utils-terminal@2.3.5': resolution: {integrity: sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==} engines: {node: '>=16.0.0'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@rollup/rollup-android-arm-eabi@4.62.2': resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} cpu: [arm] @@ -424,8 +428,9 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -438,15 +443,16 @@ packages: resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==} engines: {node: '>= 5.10.0'} - brace-expansion@2.1.1: - resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} @@ -455,9 +461,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - commander@9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} @@ -495,26 +501,22 @@ packages: fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fs-extra@11.3.5: + resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==} + engines: {node: '>=14.14'} + fs-extra@9.1.0: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} - fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - glob@9.3.5: - resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} - engines: {node: '>=16 || 14 >=14.17'} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -556,37 +558,21 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - - minimatch@8.0.7: - resolution: {integrity: sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==} - engines: {node: '>=16 || 14 >=14.17'} - - minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - - minipass@4.2.8: - resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} - engines: {node: '>=8'} + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} - minipass@5.0.0: - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} - engines: {node: '>=8'} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} - minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} - - mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -605,13 +591,16 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -638,9 +627,9 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} - rimraf@4.4.1: - resolution: {integrity: sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==} - engines: {node: '>=14'} + rimraf@6.1.3: + resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} + engines: {node: 20 || >=22} hasBin: true rollup@4.62.2: @@ -700,10 +689,9 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - tar@6.2.1: - resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} - engines: {node: '>=10'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + tar@7.5.16: + resolution: {integrity: sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==} + engines: {node: '>=18'} through2@4.0.2: resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} @@ -769,8 +757,8 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} - xml2js@0.5.0: - resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} engines: {node: '>=4.0.0'} xmlbuilder@11.0.1: @@ -781,55 +769,60 @@ packages: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} snapshots: - '@capacitor/android@6.2.1(@capacitor/core@6.2.1)': + '@capacitor/android@8.4.1(@capacitor/core@8.4.1)': dependencies: - '@capacitor/core': 6.2.1 + '@capacitor/core': 8.4.1 - '@capacitor/app@6.0.3(@capacitor/core@6.2.1)': + '@capacitor/app@8.1.0(@capacitor/core@8.4.1)': dependencies: - '@capacitor/core': 6.2.1 + '@capacitor/core': 8.4.1 - '@capacitor/cli@6.2.1': + '@capacitor/cli@8.4.1': dependencies: '@ionic/cli-framework-output': 2.2.8 - '@ionic/utils-fs': 3.1.7 - '@ionic/utils-subprocess': 2.1.11 + '@ionic/utils-subprocess': 3.0.1 '@ionic/utils-terminal': 2.3.5 - commander: 9.5.0 + commander: 12.1.0 debug: 4.4.3 env-paths: 2.2.1 + fs-extra: 11.3.5 kleur: 4.1.5 native-run: 2.0.3 open: 8.4.2 plist: 3.1.1 prompts: 2.4.2 - rimraf: 4.4.1 + rimraf: 6.1.3 semver: 7.8.5 - tar: 6.2.1 + tar: 7.5.16 tslib: 2.8.1 - xml2js: 0.5.0 + xml2js: 0.6.2 transitivePeerDependencies: - supports-color - '@capacitor/core@6.2.1': + '@capacitor/core@8.4.1': dependencies: tslib: 2.8.1 - '@capacitor/ios@6.2.1(@capacitor/core@6.2.1)': + '@capacitor/ios@8.4.1(@capacitor/core@8.4.1)': + dependencies: + '@capacitor/core': 8.4.1 + + '@capacitor/preferences@8.0.1(@capacitor/core@8.4.1)': dependencies: - '@capacitor/core': 6.2.1 + '@capacitor/core': 8.4.1 - '@capacitor/preferences@6.0.4(@capacitor/core@6.2.1)': + '@capgo/native-purchases@8.4.6(@capacitor/core@8.4.1)': dependencies: - '@capacitor/core': 6.2.1 + '@capacitor/core': 8.4.1 '@esbuild/aix-ppc64@0.21.5': optional: true @@ -908,18 +901,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@ionic/utils-array@2.1.5': - dependencies: - debug: 4.4.3 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - - '@ionic/utils-fs@3.1.6': + '@ionic/utils-array@2.1.6': dependencies: - '@types/fs-extra': 8.1.5 debug: 4.4.3 - fs-extra: 9.1.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -933,17 +917,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@ionic/utils-object@2.1.5': + '@ionic/utils-object@2.1.6': dependencies: debug: 4.4.3 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@ionic/utils-process@2.1.10': + '@ionic/utils-process@2.1.12': dependencies: - '@ionic/utils-object': 2.1.5 - '@ionic/utils-terminal': 2.3.3 + '@ionic/utils-object': 2.1.6 + '@ionic/utils-terminal': 2.3.5 debug: 4.4.3 signal-exit: 3.0.7 tree-kill: 1.2.2 @@ -951,27 +935,27 @@ snapshots: transitivePeerDependencies: - supports-color - '@ionic/utils-stream@3.1.5': + '@ionic/utils-stream@3.1.7': dependencies: debug: 4.4.3 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@ionic/utils-subprocess@2.1.11': + '@ionic/utils-subprocess@3.0.1': dependencies: - '@ionic/utils-array': 2.1.5 - '@ionic/utils-fs': 3.1.6 - '@ionic/utils-process': 2.1.10 - '@ionic/utils-stream': 3.1.5 - '@ionic/utils-terminal': 2.3.3 + '@ionic/utils-array': 2.1.6 + '@ionic/utils-fs': 3.1.7 + '@ionic/utils-process': 2.1.12 + '@ionic/utils-stream': 3.1.7 + '@ionic/utils-terminal': 2.3.5 cross-spawn: 7.0.6 debug: 4.4.3 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@ionic/utils-terminal@2.3.3': + '@ionic/utils-terminal@2.3.5': dependencies: '@types/slice-ansi': 4.0.0 debug: 4.4.3 @@ -985,19 +969,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@ionic/utils-terminal@2.3.5': + '@isaacs/fs-minipass@4.0.1': dependencies: - '@types/slice-ansi': 4.0.0 - debug: 4.4.3 - signal-exit: 3.0.7 - slice-ansi: 4.0.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - tslib: 2.8.1 - untildify: 4.0.0 - wrap-ansi: 7.0.0 - transitivePeerDependencies: - - supports-color + minipass: 7.1.3 '@rollup/rollup-android-arm-eabi@4.62.2': optional: true @@ -1102,7 +1076,7 @@ snapshots: at-least-node@1.0.0: {} - balanced-match@1.0.2: {} + balanced-match@4.0.4: {} base64-js@1.5.1: {} @@ -1112,13 +1086,13 @@ snapshots: dependencies: big-integer: 1.6.52 - brace-expansion@2.1.1: + brace-expansion@5.0.6: dependencies: - balanced-match: 1.0.2 + balanced-match: 4.0.4 buffer-crc32@0.2.13: {} - chownr@2.0.0: {} + chownr@3.0.0: {} color-convert@2.0.1: dependencies: @@ -1126,7 +1100,7 @@ snapshots: color-name@1.1.4: {} - commander@9.5.0: {} + commander@12.1.0: {} cross-spawn@7.0.6: dependencies: @@ -1178,28 +1152,27 @@ snapshots: dependencies: pend: 1.2.0 - fs-extra@9.1.0: + fs-extra@11.3.5: dependencies: - at-least-node: 1.0.0 graceful-fs: 4.2.11 jsonfile: 6.2.1 universalify: 2.0.1 - fs-minipass@2.1.0: + fs-extra@9.1.0: dependencies: - minipass: 3.3.6 - - fs.realpath@1.0.0: {} + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 fsevents@2.3.3: optional: true - glob@9.3.5: + glob@13.0.6: dependencies: - fs.realpath: 1.0.0 - minimatch: 8.0.7 - minipass: 4.2.8 - path-scurry: 1.11.1 + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 graceful-fs@4.2.11: {} @@ -1229,28 +1202,17 @@ snapshots: kleur@4.1.5: {} - lru-cache@10.4.3: {} - - minimatch@8.0.7: - dependencies: - brace-expansion: 2.1.1 + lru-cache@11.5.1: {} - minipass@3.3.6: + minimatch@10.2.5: dependencies: - yallist: 4.0.0 - - minipass@4.2.8: {} - - minipass@5.0.0: {} + brace-expansion: 5.0.6 minipass@7.1.3: {} - minizlib@2.1.2: + minizlib@3.1.0: dependencies: - minipass: 3.3.6 - yallist: 4.0.0 - - mkdirp@1.0.4: {} + minipass: 7.1.3 ms@2.1.3: {} @@ -1278,11 +1240,13 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + package-json-from-dist@1.0.1: {} + path-key@3.1.1: {} - path-scurry@1.11.1: + path-scurry@2.0.2: dependencies: - lru-cache: 10.4.3 + lru-cache: 11.5.1 minipass: 7.1.3 pend@1.2.0: {} @@ -1314,9 +1278,10 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 - rimraf@4.4.1: + rimraf@6.1.3: dependencies: - glob: 9.3.5 + glob: 13.0.6 + package-json-from-dist: 1.0.1 rollup@4.62.2: dependencies: @@ -1391,14 +1356,13 @@ snapshots: dependencies: ansi-regex: 5.0.1 - tar@6.2.1: + tar@7.5.16: dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 5.0.0 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 through2@4.0.2: dependencies: @@ -1435,7 +1399,7 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - xml2js@0.5.0: + xml2js@0.6.2: dependencies: sax: 1.6.0 xmlbuilder: 11.0.1 @@ -1444,7 +1408,7 @@ snapshots: xmlbuilder@15.1.1: {} - yallist@4.0.0: {} + yallist@5.0.0: {} yauzl@2.10.0: dependencies: diff --git a/mobile/src/iap.js b/mobile/src/iap.js index 0ecf036..0147269 100644 --- a/mobile/src/iap.js +++ b/mobile/src/iap.js @@ -3,25 +3,88 @@ // 方針: 無料ダウンロード + 非消耗型(non-consumable)1 点を App Store / Google Play で購入 → クラウド同期解禁。 // ストア依存を 1 ファイルに閉じ、UI とオーケストレータは isUnlocked()/purchase()/restore() だけ見る。 // -// 実装 TODO(ネイティブ): @revenuecat/purchases-capacitor か @capacitor-community 系の IAP プラグインを配線する。 -// - product id 例: "jp.nephilim.petarin.sync"(App Store / Play で同一 non-consumable を登録) -// - 起動時に restore(購入復元)→ entitlement をローカル(Preferences)にキャッシュ。 -// - レシート検証は当面クライアント側(プラグインの検証)に委ね、後段で sekisho(サブスク基盤)に寄せて -// relay 側 enforcement(購入者のみ relay 受理)へ強化する。 +// ネイティブ配線: @capgo/native-purchases(StoreKit 2 / Google Play Billing 直叩き・外部 SaaS 不要)。 +// - product id: "jp.nephilim.petarin.sync"(App Store / Play で同一 non-consumable を登録)。 +// - 起動時にストアへ所有照会(getPurchases)→ 結果を Preferences にキャッシュ(オフライン起動の即時表示用)。 +// - レシート検証は当面クライアント側(ストア API の所有判定)に委ね、後段で sekisho(サブスク基盤)へ寄せて +// relay 側 enforcement(購入者のみ relay 受理)へ強化する余地を残す。 // -// 現状: ブラウザ(Vite dev)や未配線ネイティブでは「開発用解錠フラグ」を localStorage で見る=検証用。 -// 実機リリースでは下の TODO を埋めるまでクラウド同期は購入導線のみ(解錠しない)。 +// プラットフォーム別の所有判定(買い切り): +// - iOS: Transaction.currentEntitlements(onlyCurrentEntitlements:true)に non-consumable が在る=所有。 +// 別 Apple ID 端末での購入漏洩を防ぐため currentEntitlements に絞る。 +// - Android: purchaseState==="1"(PURCHASED) かつ acknowledged のものだけ有効。 +// +// web(Vite dev) では @capgo はネイティブ専用=呼ばない。代わりに開発用解錠フラグ(localStorage)を見る。 +// 本番ビルドでは import.meta.env.DEV が false に静的置換され localStorage 分岐ごと dead code になる +// (DevTools で petarin:dev:unlocked を立てても解錠されない=無課金バイパス防止)。 + +import { Capacitor } from "@capacitor/core"; +import { Preferences } from "@capacitor/preferences"; + +export const PRODUCT_ID = "jp.nephilim.petarin.sync"; +const UNLOCK_CACHE_KEY = "petarin:iap:unlocked"; // ストア照会結果のキャッシュ(真実の源はストア) -// 本番ビルドでは DEV 解錠フラグを一切信用しない。import.meta.env.DEV は Vite が dev=true/ -// 本番ビルド=false に静的置換するため、本番では下の localStorage 分岐ごと dead code になり -// DevTools で petarin:dev:unlocked を立てても解錠されない(無課金バイパス防止)。 const IS_DEV = typeof import.meta !== "undefined" && !!import.meta.env?.DEV; const DEV_UNLOCK_KEY = "petarin:dev:unlocked"; let _unlocked = false; +function isNative() { + try { + return Capacitor.isNativePlatform(); + } catch { + return false; + } +} + +// @capgo はネイティブ専用。web で評価されても呼ばないよう動的 import(別チャンク化)。 +let _plugin = null; +async function plugin() { + if (!_plugin) { + const m = await import("@capgo/native-purchases"); + _plugin = { NativePurchases: m.NativePurchases, PURCHASE_TYPE: m.PURCHASE_TYPE }; + } + return _plugin; +} + +// ストアに「この買い切りを現在所有しているか」を問い合わせる(true/false)。例外は呼び出し側で握る。 +async function queryOwned() { + const { NativePurchases, PURCHASE_TYPE } = await plugin(); + const sup = await NativePurchases.isBillingSupported().catch(() => ({ isBillingSupported: false })); + if (!sup.isBillingSupported) return false; + const { purchases } = await NativePurchases.getPurchases({ + productType: PURCHASE_TYPE.INAPP, + onlyCurrentEntitlements: true, // iOS: 現権利のみ=別 Apple ID 端末の購入漏洩を防ぐ + }); + const mine = (purchases || []).filter((p) => p.productIdentifier === PRODUCT_ID); + if (!mine.length) return false; + if (Capacitor.getPlatform() === "android") { + // Android は PURCHASED(="1") かつ acknowledged のものだけ有効(PENDING や未承認は除外) + return mine.some((p) => (p.purchaseState === "1" || p.purchaseState === "PURCHASED") && p.isAcknowledged !== false); + } + // iOS: currentEntitlements に non-consumable が在る=所有 + return true; +} + export async function initIap() { - // TODO: 本番はネイティブ IAP プラグイン初期化 + restorePurchases() で _unlocked を確定。 + if (isNative()) { + // 1) Preferences キャッシュで即時復元(オフライン起動でも前回の解錠状態を即反映) + try { + const { value } = await Preferences.get({ key: UNLOCK_CACHE_KEY }); + _unlocked = value === "1"; + } catch { + _unlocked = false; + } + // 2) ストアで真実を確認しキャッシュ更新。失敗(オフライン等)はキャッシュ値を維持する。 + try { + _unlocked = await queryOwned(); + await Preferences.set({ key: UNLOCK_CACHE_KEY, value: _unlocked ? "1" : "0" }); + } catch { + /* オフライン/一時失敗はキャッシュ値のまま */ + } + return _unlocked; + } + // web(dev のみ): 検証用の解錠フラグ。本番 web ビルドは存在しないが import.meta.env.DEV で二重に塞ぐ。 if (!IS_DEV) { _unlocked = false; return _unlocked; @@ -38,9 +101,28 @@ export function isUnlocked() { return _unlocked; } +// 買い切りを購入する。成功で true。キャンセル/失敗は purchaseProduct が throw する=呼び出し側で握る。 export async function purchase() { - // TODO: 本番はネイティブ IAP の purchase(productId) 成功時のみ _unlocked=true にして Preferences へ保存。 - // 本番ビルドでは解錠しない(無課金バイパス防止)。dev のみ検証用に解錠フラグを立てる。 + if (isNative()) { + const { NativePurchases, PURCHASE_TYPE } = await plugin(); + const tx = await NativePurchases.purchaseProduct({ + productIdentifier: PRODUCT_ID, + productType: PURCHASE_TYPE.INAPP, // Android 用(iOS は無視)。買い切り。autoAcknowledge は既定 true。 + quantity: 1, + }); + // 例外を投げなければ購入成立。念のため productIdentifier 一致を確認し、ストア所有照会で最終確定する。 + const ok = !!tx && tx.productIdentifier === PRODUCT_ID; + _unlocked = ok ? true : await queryOwned().catch(() => false); + if (_unlocked) { + try { + await Preferences.set({ key: UNLOCK_CACHE_KEY, value: "1" }); + } catch { + /* noop */ + } + } + return _unlocked; + } + // web(dev のみ): 検証用に解錠。本番 web は解錠しない(無課金バイパス防止)。 if (!IS_DEV) return false; _unlocked = true; try { @@ -51,7 +133,16 @@ export async function purchase() { return _unlocked; } +// 購入復元(機種変更・再インストール時)。iOS は restorePurchases で過去購入を再同期してから再判定。 export async function restore() { - // TODO: プラグインの restorePurchases()。 + if (isNative()) { + try { + const { NativePurchases } = await plugin(); + await NativePurchases.restorePurchases(); + } catch { + /* restore 失敗でも下の initIap()→queryOwned で確認する */ + } + return initIap(); + } return initIap(); } diff --git a/mobile/src/main.js b/mobile/src/main.js index a002982..07a744b 100644 --- a/mobile/src/main.js +++ b/mobile/src/main.js @@ -368,11 +368,12 @@ async function renderPairing() { async function onBuy() { setNote("購入処理中…"); try { - await purchase(); + const ok = await purchase(); + if (!ok) return setNote("購入が確認できませんでした。", true); // 未解錠で「解禁」と誤表示しない await renderPairing(); setNote("クラウド同期を解禁しました。グループを作成するか、PC で表示したコードで参加してください。"); } catch { - setNote("購入に失敗しました。", true); + setNote("購入に失敗しました(キャンセルまたはエラー)。", true); } } @@ -427,8 +428,10 @@ async function onCopy() { } // ── QR カメラスキャン(getUserMedia + jsQR)。検出したコードで onJoin を自動実行 ── -// Web(Safari)は HTTPS=secure context で動く。ネイティブ(WKWebView)はカメラ権限(Info.plist の -// NSCameraUsageDescription)が要る/必要なら barcode-scanner プラグインへ差し替える(TODO)。 +// Web(Safari)は HTTPS=secure context で動く。iOS の WKWebView は 14.3+ で getUserMedia 対応= +// Info.plist の NSCameraUsageDescription があれば自前アプリの WebView でカメラが使える(CI で注入・ +// deployment target 15.0)。Android は AndroidManifest の CAMERA 権限が要る(CI で注入)。 +// ※ WebKit bug #208667 は「Chrome/Firefox 等サードパーティ WKWebView ブラウザ」の話で自前アプリには当たらない。 let scanStream = null; let scanRAF = 0; async function openScanner() { From d5a17b94896dc8f6d649a7aaa83b883b74aa2763 Mon Sep 17 00:00:00 2001 From: IMT <1llum1n4t1@duck.com> Date: Tue, 23 Jun 2026 20:45:11 +0900 Subject: [PATCH 2/4] =?UTF-8?q?ci(mobile):=20Android=20=E3=82=92=20Cap8(JD?= =?UTF-8?q?K21)+=E8=AA=B2=E9=87=91/=E3=82=AB=E3=83=A1=E3=83=A9=E6=A8=A9?= =?UTF-8?q?=E9=99=90=E3=81=B8=E3=83=BBiOS=20=E3=83=93=E3=83=AB=E3=83=89?= =?UTF-8?q?=E6=A4=9C=E8=A8=BCCI=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mobile-android.yml: Cap8 テンプレが JavaVersion.VERSION_21 を使うため JDK 17→21。 AndroidManifest へ CAMERA(QRスキャナ)に加え BILLING(@capgo課金)も注入。 - mobile-ios.yml(新規): macOS・SPM。cap add ios→Info.plist へ NSCameraUsageDescription 注入→署名なし simulator コンパイル検証。実機.ipa/署名/申請は証明書投入後。 Cap8 は Xcode26+ 要求=runner の Xcode 次第。 Co-Authored-By: Claude Opus 4.8 --- .github/workflows/mobile-android.yml | 14 ++++-- .github/workflows/mobile-ios.yml | 68 ++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/mobile-ios.yml diff --git a/.github/workflows/mobile-android.yml b/.github/workflows/mobile-android.yml index 21faa5e..492a6bd 100644 --- a/.github/workflows/mobile-android.yml +++ b/.github/workflows/mobile-android.yml @@ -32,10 +32,12 @@ jobs: - name: Enable corepack (pnpm) run: corepack enable + # Capacitor 8 のテンプレは capacitor.build.gradle で JavaVersion.VERSION_21=JDK 21 必須 + # (AGP 8.13 / Gradle 8.14.3)。17 だと Java 21 ターゲットでコンパイルエラーになる。 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: temurin - java-version: "17" + java-version: "21" - name: Install deps run: pnpm install --ignore-workspace --config.dangerouslyAllowAllBuilds=true @@ -49,14 +51,18 @@ jobs: - name: Capacitor sync run: pnpm exec cap sync android - - name: Add CAMERA permission to AndroidManifest (QR スキャナの getUserMedia 用) + - name: AndroidManifest へ権限注入 (CAMERA=QRスキャナ / BILLING=アプリ内課金) working-directory: mobile/android run: | - # 生 getUserMedia でカメラを使うため CAMERA 権限を注入する(Capacitor は自動付与しない)。 - # 無いとネイティブビルドの QR スキャンが実行時にパーミッションエラーで失敗する。 + # CAMERA: 生 getUserMedia でカメラを使うため注入(Capacitor は自動付与しない)。無いと QR スキャンが + # 実行時にパーミッションエラーで失敗する。 + # BILLING: @capgo/native-purchases の課金に com.android.vending.BILLING が要る。Play Billing ライブラリが + # merge するはずだが、library 依存に頼らず明示注入して確実化(manifest merge は重複を 1 つに畳む)。 manifest=app/src/main/AndroidManifest.xml grep -q 'android.permission.CAMERA' "$manifest" || \ sed -i 's## \n#' "$manifest" + grep -q 'com.android.vending.BILLING' "$manifest" || \ + sed -i 's## \n#' "$manifest" echo "--- AndroidManifest.xml の権限 ---"; grep -n 'uses-permission' "$manifest" || true - name: Gradle assembleDebug diff --git a/.github/workflows/mobile-ios.yml b/.github/workflows/mobile-ios.yml new file mode 100644 index 0000000..7b25b64 --- /dev/null +++ b/.github/workflows/mobile-ios.yml @@ -0,0 +1,68 @@ +# ぺたりん モバイル: iOS ビルド検証(Capacitor 8・Swift Package Manager)。 +# web を Vite でビルド → cap add ios(毎回生成・ios/ は gitignore)→ Info.plist に NSCameraUsageDescription を +# 注入(QR スキャナの getUserMedia 用・Android の CAMERA 注入と対称)→ 署名なしで simulator 向けにコンパイル検証。 +# 署名・実機 .ipa・App Store 申請は Apple Developer 証明書(macOS/署名)が要るので別途。ここはコンパイルが通るかの検証。 +# Capacitor 8 は Xcode 26+ を要求=runner の Xcode 次第(未達ならこの job は環境要件待ちとして赤くなる)。 +# Actions はサプライチェーン対策で full commit SHA にピン留め(publish.yml / mobile-android.yml と同方針・Dependabot 更新)。 +name: mobile-ios + +on: + workflow_dispatch: {} + push: + tags: + - "mobile-v*" + +permissions: + contents: read + +jobs: + build: + runs-on: macos-latest + defaults: + run: + working-directory: mobile + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.0 + with: + persist-credentials: false # 読み取り専用ビルド=認証情報を .git/config に残さない + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.0.0 + with: + node-version: 22 + package-manager-cache: false # PR 由来の cache poisoning を release に伝播させない + + - name: Enable corepack (pnpm) + run: corepack enable + + - name: Install deps + run: pnpm install --ignore-workspace --config.dangerouslyAllowAllBuilds=true + + - name: Build web (Vite) + run: pnpm build + + - name: Add iOS platform + run: pnpm exec cap add ios + + - name: Capacitor sync + run: pnpm exec cap sync ios + + - name: Info.plist へ NSCameraUsageDescription 注入 (QR スキャナの getUserMedia 用) + working-directory: mobile/ios + run: | + # iOS 14.3+ の WKWebView は getUserMedia 対応だが、Info.plist のカメラ用途説明が無いと実行時に拒否される。 + # Android の CAMERA 注入と対称に「生成物に依存せず再現可能」に注入する(Delete→Add で冪等)。 + plist=App/App/Info.plist + /usr/libexec/PlistBuddy -c "Delete :NSCameraUsageDescription" "$plist" 2>/dev/null || true + /usr/libexec/PlistBuddy -c "Add :NSCameraUsageDescription string 'QRコードを読み取ってグループに参加するためにカメラを使用します'" "$plist" + echo "--- Info.plist のカメラ用途 ---"; /usr/libexec/PlistBuddy -c "Print :NSCameraUsageDescription" "$plist" + + - name: Xcode で署名なしコンパイル検証 (simulator) + working-directory: mobile/ios/App + run: | + # 署名・provisioning は Apple Developer 証明書が要るので CI ではコンパイル検証のみ(simulator SDK)。 + # 実機 .ipa / App Store 申請は別(証明書を Secrets に入れてから署名ビルドを足す)。 + # In-App Purchase(StoreKit2) は entitlement 不要=App Store Connect の product 登録で機能する。 + set -o pipefail + xcodebuild -project App.xcodeproj -scheme App -configuration Debug \ + -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' \ + CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO build | tail -40 From 48b6973a36a00a9e36eac9bca0a625f35623e01f Mon Sep 17 00:00:00 2001 From: IMT <1llum1n4t1@duck.com> Date: Wed, 24 Jun 2026 08:57:24 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix(mobile):=20PR#9=20=E3=83=AC=E3=83=93?= =?UTF-8?q?=E3=83=A5=E3=83=BC=E5=AF=BE=E5=BF=9C=20=E2=80=94=20=E8=AA=B2?= =?UTF-8?q?=E9=87=91=E6=89=80=E6=9C=89=E5=88=A4=E5=AE=9A=E3=81=AE=E5=8E=B3?= =?UTF-8?q?=E5=AF=86=E5=8C=96=EF=BC=8BCI=E4=BE=9B=E7=B5=A6=E7=B6=B2?= =?UTF-8?q?=E3=83=AA=E3=82=B9=E3=82=AF=E4=BD=8E=E6=B8=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iap.js(課金): - Android 所有判定: purchaseState は数値 1 / 文字列 "1" / "PURCHASED" を許容、isAcknowledged は厳密 true を 要求(!==false は undefined を通し、3日で自動払戻しされる未承認購入を所有扱いにする穴)。 - purchase: productIdentifier 一致では解錠せず必ず queryOwned() で確定(iOS Ask to Buy 等の deferred を 未承認のまま解禁しない)。 - isBillingSupported の throw を握り潰さず伝播=initIap が catch して Preferences キャッシュを維持 (一時障害/オフラインで購入者をロックアウトしない)。 CI: - mobile-ios.yml: runs-on を macos-latest→macos-15 固定(イメージ更新による Xcode 非決定性を排除)。 - 両 mobile CI: --config.dangerouslyAllowAllBuilds=true を廃し mobile/pnpm-workspace.yaml の allowBuilds(esbuild のみ)で許可。--ignore-workspace も外す(付けると allowBuilds が読まれず ignored builds)。 検証: vite build / _sync_repro 223 / _mobile_crud_repro 15 緑。 Co-Authored-By: Claude Opus 4.8 --- .github/workflows/mobile-android.yml | 5 ++++- .github/workflows/mobile-ios.yml | 9 +++++++-- mobile/pnpm-workspace.yaml | 6 ++++++ mobile/src/iap.js | 23 ++++++++++++++++------- 4 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 mobile/pnpm-workspace.yaml diff --git a/.github/workflows/mobile-android.yml b/.github/workflows/mobile-android.yml index 492a6bd..3bed185 100644 --- a/.github/workflows/mobile-android.yml +++ b/.github/workflows/mobile-android.yml @@ -39,8 +39,11 @@ jobs: distribution: temurin java-version: "21" + # 供給網リスク低減: 全ビルド許可せず mobile/pnpm-workspace.yaml の allowBuilds(esbuild のみ)で許可する。 + # --ignore-workspace を付けると pnpm-workspace.yaml=allowBuilds が読まれず ignored builds になるので付けない + #(mobile/pnpm-workspace.yaml があるため上位の repo ルート workspace には巻き込まれない)。 - name: Install deps - run: pnpm install --ignore-workspace --config.dangerouslyAllowAllBuilds=true + run: pnpm install - name: Build web (Vite) run: pnpm build diff --git a/.github/workflows/mobile-ios.yml b/.github/workflows/mobile-ios.yml index 7b25b64..04066e0 100644 --- a/.github/workflows/mobile-ios.yml +++ b/.github/workflows/mobile-ios.yml @@ -17,7 +17,9 @@ permissions: jobs: build: - runs-on: macos-latest + # macos-latest はイメージ更新で Xcode が変わり「Xcode 26+ 必須」を非決定的にするため固定する。 + # Cap8 要件の Xcode が無ければこの job は失敗=要件未達が可視化される(必要なら setup-xcode で pin)。 + runs-on: macos-15 defaults: run: working-directory: mobile @@ -34,8 +36,11 @@ jobs: - name: Enable corepack (pnpm) run: corepack enable + # 供給網リスク低減: 全ビルド許可せず mobile/pnpm-workspace.yaml の allowBuilds(esbuild のみ)で許可する。 + # --ignore-workspace を付けると pnpm-workspace.yaml=allowBuilds が読まれず ignored builds になるので付けない + #(mobile/pnpm-workspace.yaml があるため上位の repo ルート workspace には巻き込まれない)。 - name: Install deps - run: pnpm install --ignore-workspace --config.dangerouslyAllowAllBuilds=true + run: pnpm install - name: Build web (Vite) run: pnpm build diff --git a/mobile/pnpm-workspace.yaml b/mobile/pnpm-workspace.yaml new file mode 100644 index 0000000..f1a8d5e --- /dev/null +++ b/mobile/pnpm-workspace.yaml @@ -0,0 +1,6 @@ +# pnpm のビルドスクリプト許可(供給網リスク低減=`dangerouslyAllowAllBuilds` で全許可せず、postinstall +# ビルドが要る依存だけ true にする)。pnpm v11 は allowBuilds で許可/拒否を明示し、未列挙はデフォルト拒否 +# =未レビュー扱い(strictDepBuilds)。esbuild は vite が使うネイティブ binary を postinstall で配置=ビルド必須。 +# @capacitor/* / @capgo はネイティブプラグインだが Node 側 postinstall は持たない(gradle/SPM 側でビルド)=許可不要。 +allowBuilds: + esbuild: true diff --git a/mobile/src/iap.js b/mobile/src/iap.js index 0147269..f25e027 100644 --- a/mobile/src/iap.js +++ b/mobile/src/iap.js @@ -50,7 +50,9 @@ async function plugin() { // ストアに「この買い切りを現在所有しているか」を問い合わせる(true/false)。例外は呼び出し側で握る。 async function queryOwned() { const { NativePurchases, PURCHASE_TYPE } = await plugin(); - const sup = await NativePurchases.isBillingSupported().catch(() => ({ isBillingSupported: false })); + // isBillingSupported の throw(接続一時エラー等)は握り潰さず呼び出し側へ伝播させる。initIap は + // それを catch して Preferences キャッシュを維持=オフライン/一時障害で購入者をロックアウトしない。 + const sup = await NativePurchases.isBillingSupported(); if (!sup.isBillingSupported) return false; const { purchases } = await NativePurchases.getPurchases({ productType: PURCHASE_TYPE.INAPP, @@ -59,8 +61,14 @@ async function queryOwned() { const mine = (purchases || []).filter((p) => p.productIdentifier === PRODUCT_ID); if (!mine.length) return false; if (Capacitor.getPlatform() === "android") { - // Android は PURCHASED(="1") かつ acknowledged のものだけ有効(PENDING や未承認は除外) - return mine.some((p) => (p.purchaseState === "1" || p.purchaseState === "PURCHASED") && p.isAcknowledged !== false); + // Android は PURCHASED かつ acknowledged のものだけ有効(PENDING・未承認は除外)。 + // purchaseState は数値 1 でも文字列 "1" でも来うる。isAcknowledged は厳密に true を要求する + //(undefined を通すと Google Play の承認要件違反=3日で自動払い戻しされる購入を所有扱いにしてしまう)。 + return mine.some( + (p) => + (p.purchaseState === 1 || p.purchaseState === "1" || p.purchaseState === "PURCHASED") && + p.isAcknowledged === true + ); } // iOS: currentEntitlements に non-consumable が在る=所有 return true; @@ -105,14 +113,15 @@ export function isUnlocked() { export async function purchase() { if (isNative()) { const { NativePurchases, PURCHASE_TYPE } = await plugin(); - const tx = await NativePurchases.purchaseProduct({ + await NativePurchases.purchaseProduct({ productIdentifier: PRODUCT_ID, productType: PURCHASE_TYPE.INAPP, // Android 用(iOS は無視)。買い切り。autoAcknowledge は既定 true。 quantity: 1, }); - // 例外を投げなければ購入成立。念のため productIdentifier 一致を確認し、ストア所有照会で最終確定する。 - const ok = !!tx && tx.productIdentifier === PRODUCT_ID; - _unlocked = ok ? true : await queryOwned().catch(() => false); + // productIdentifier 一致だけでは解錠しない。iOS の Ask to Buy(deferred) 等で purchaseProduct が + // 例外を投げずに保留トランザクションを返すことがあり、未承認のまま解禁してしまうため。必ずストア所有照会 + //(queryOwned=Android は acknowledged 厳密確認・iOS は currentEntitlements)で確定してから解錠する。 + _unlocked = await queryOwned().catch(() => false); if (_unlocked) { try { await Preferences.set({ key: UNLOCK_CACHE_KEY, value: "1" }); From ab0010b89617a6e934928204e0c2bb31d839879f Mon Sep 17 00:00:00 2001 From: IMT <1llum1n4t1@duck.com> Date: Wed, 24 Jun 2026 09:02:58 +0900 Subject: [PATCH 4/4] =?UTF-8?q?ci(mobile):=20pnpm=20install=20=E3=81=AB=20?= =?UTF-8?q?--frozen-lockfile=20=E3=82=92=E4=BB=98=E4=B8=8E=EF=BC=88lockfil?= =?UTF-8?q?e=20drift=20=E6=A4=9C=E5=87=BA=E3=83=BB=E5=86=8D=E7=8F=BE?= =?UTF-8?q?=E6=80=A7=E5=9B=BA=E5=AE=9A=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit nitpick 対応。両 mobile CI の install を frozen-lockfile 化=lockfile と package.json の不整合を CI で検出し、依存解決を再現可能にする。ローカルで通過確認済み。 Co-Authored-By: Claude Opus 4.8 --- .github/workflows/mobile-android.yml | 2 +- .github/workflows/mobile-ios.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mobile-android.yml b/.github/workflows/mobile-android.yml index 3bed185..86a1ead 100644 --- a/.github/workflows/mobile-android.yml +++ b/.github/workflows/mobile-android.yml @@ -43,7 +43,7 @@ jobs: # --ignore-workspace を付けると pnpm-workspace.yaml=allowBuilds が読まれず ignored builds になるので付けない #(mobile/pnpm-workspace.yaml があるため上位の repo ルート workspace には巻き込まれない)。 - name: Install deps - run: pnpm install + run: pnpm install --frozen-lockfile # lockfile を固定=CI で依存解決の非決定性/drift を防ぐ - name: Build web (Vite) run: pnpm build diff --git a/.github/workflows/mobile-ios.yml b/.github/workflows/mobile-ios.yml index 04066e0..82429c8 100644 --- a/.github/workflows/mobile-ios.yml +++ b/.github/workflows/mobile-ios.yml @@ -40,7 +40,7 @@ jobs: # --ignore-workspace を付けると pnpm-workspace.yaml=allowBuilds が読まれず ignored builds になるので付けない #(mobile/pnpm-workspace.yaml があるため上位の repo ルート workspace には巻き込まれない)。 - name: Install deps - run: pnpm install + run: pnpm install --frozen-lockfile # lockfile を固定=CI で依存解決の非決定性/drift を防ぐ - name: Build web (Vite) run: pnpm build