From 48ca19f661e48680e7663f5abe7748b5681a24d0 Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 25 May 2026 02:32:25 +0900 Subject: [PATCH 01/51] feat(runtime): add fire os and vega support Add Amazon Fire OS build support, Vega runtime adapters, generated type updates, docs, and kit-side Amazon receipt handling. Validation: - packages/docs: bun run typecheck - packages/google: ./gradlew :openiap:compilePlayDebugKotlin - packages/google: ./gradlew :openiap:compileHorizonDebugKotlin - scripts/audit-non-godot-parity.mjs - packages/gql: bun test failed under local Bun 1.1.0 HTTP server compatibility --- .github/workflows/ci.yml | 4 + .github/workflows/release-google.yml | 53 +- knowledge/_claude-context/context.md | 30 +- knowledge/internal/03-coding-style.md | 29 + knowledge/internal/06-git-deployment.md | 2 +- libraries/expo-iap/.npmignore | 4 + libraries/expo-iap/android/build.gradle | 24 +- .../main/java/expo/modules/iap/ExpoIapLog.kt | 9 +- .../java/expo/modules/iap/ExpoIapModule.kt | 11 +- libraries/expo-iap/bun.lock | 6 + libraries/expo-iap/example/app.config.ts | 10 +- libraries/expo-iap/ios/ExpoIapLog.swift | 9 +- libraries/expo-iap/ios/ExpoIapModule.swift | 8 +- libraries/expo-iap/package.json | 12 +- .../expo-iap/plugin/__tests__/withIAP.test.ts | 148 ++- .../plugin/src/expoConfig.augmentation.d.ts | 24 +- libraries/expo-iap/plugin/src/withIAP.ts | 337 ++++--- .../expo-iap/plugin/src/withLocalOpenIAP.ts | 35 +- libraries/expo-iap/src/ExpoIapModule.ts | 127 +-- .../__tests__/native-log-redaction.test.js | 62 ++ .../src/__tests__/vega-adapter.test.ts | 509 +++++++++++ .../expo-iap/src/amazon-devices-kepler.d.ts | 12 + libraries/expo-iap/src/index.ts | 19 +- libraries/expo-iap/src/types.ts | 20 +- libraries/expo-iap/src/vega-adapter.ts | 847 +++++++++++++++++ libraries/expo-iap/src/vega.kepler.ts | 24 + libraries/expo-iap/src/vega.ts | 10 + .../android/build.gradle | 19 +- .../android/settings.gradle | 25 +- .../AndroidInappPurchasePlugin.kt | 5 +- .../FlutterInappPurchasePlugin.kt | 10 +- .../flutter_inapp_purchase/example/README.md | 33 +- .../example/android/app/build.gradle | 8 +- .../example/android/gradle.properties | 5 + .../example/android/settings.gradle | 24 + .../flutter_inapp_purchase/lib/types.dart | 48 +- libraries/flutter_inapp_purchase/pubspec.yaml | 2 +- .../godot-iap/addons/godot-iap/plugin.cfg | 2 +- libraries/godot-iap/addons/godot-iap/types.gd | 53 +- libraries/kmp-iap/gradle.properties | 2 +- .../io/github/hyochan/kmpiap/openiap/Types.kt | 54 +- .../src/OpenIap.Maui/OpenIap.Maui.csproj | 2 +- libraries/maui-iap/src/OpenIap.Maui/Types.cs | 30 +- libraries/react-native-iap/README.md | 2 +- .../react-native-iap/android/build.gradle | 18 +- .../java/com/margelo/nitro/iap/HybridRnIap.kt | 23 +- .../example/android/app/build.gradle | 12 +- .../example/android/gradle.properties | 5 + .../example/android/settings.gradle | 21 +- libraries/react-native-iap/package.json | 22 +- .../plugin/__tests__/withIAP-android.test.ts | 116 +++ .../plugin/__tests__/withIAP-ios.test.ts | 11 + .../react-native-iap/plugin/src/withIAP.ts | 174 +++- .../src/__tests__/index.test.ts | 84 ++ .../src/__tests__/utils/type-bridge.test.ts | 67 +- .../src/__tests__/vega-adapter.test.ts | 509 +++++++++++ libraries/react-native-iap/src/index.ts | 98 +- .../react-native-iap/src/specs/RnIap.nitro.ts | 15 +- libraries/react-native-iap/src/types.ts | 20 +- .../types/amazon-devices-kepler/index.d.ts | 12 + .../react-native-iap/src/utils/type-bridge.ts | 3 + .../react-native-iap/src/vega-adapter.ts | 849 ++++++++++++++++++ libraries/react-native-iap/src/vega.kepler.ts | 21 + libraries/react-native-iap/src/vega.ts | 10 + libraries/react-native-iap/yarn.lock | 7 + llms-full.txt | 75 +- llms.txt | 30 +- openiap-versions.json | 2 +- packages/apple/Sources/Models/Types.swift | 31 +- packages/apple/Sources/OpenIapModule.swift | 217 ++--- packages/docs/openiap-versions.json | 2 +- packages/docs/public/llms-full.txt | 75 +- packages/docs/public/llms.txt | 30 +- .../docs/src/generated/version-metadata.json | 12 +- .../docs/src/pages/docs/android-setup.tsx | 7 +- packages/docs/src/pages/docs/ecosystem.tsx | 13 +- .../alternative-marketplace/onside.tsx | 2 +- .../docs/features/offer-code-redemption.tsx | 2 +- .../docs/features/runtime-integrations.tsx | 84 ++ .../src/pages/docs/features/validation.tsx | 8 +- .../docs/src/pages/docs/features/vega-os.tsx | 263 ++++++ packages/docs/src/pages/docs/fireos-setup.tsx | 260 ++++++ packages/docs/src/pages/docs/index.tsx | 22 +- packages/docs/src/pages/docs/setup/expo.tsx | 52 +- .../docs/src/pages/docs/setup/flutter.tsx | 15 +- packages/docs/src/pages/docs/setup/index.tsx | 8 + .../src/pages/docs/setup/react-native.tsx | 25 +- .../docs/src/pages/docs/updates/releases.tsx | 162 ++++ packages/docs/vite.config.ts | 21 + packages/google/CONTRIBUTING.md | 34 +- packages/google/CONVENTION.md | 40 +- packages/google/Example/build.gradle.kts | 6 + packages/google/openiap/build.gradle.kts | 71 +- .../openiap/src/amazon/AndroidManifest.xml | 20 + .../dev/hyo/openiap/OpenIapErrorExtensions.kt | 26 + .../java/dev/hyo/openiap/OpenIapModule.kt | 768 ++++++++++++++++ .../openiap/store/OpenIapStoreExtensions.kt | 25 + .../src/main/java/dev/hyo/openiap/Types.kt | 53 +- .../dev/hyo/openiap/store/OpenIapStore.kt | 36 +- .../utils/PurchaseVerificationValidator.kt | 38 +- .../java/dev/hyo/openiap/OpenIapErrorTest.kt | 6 +- .../PurchaseVerificationValidatorTest.kt | 99 +- .../hyo/openiap/FetchProductsAmazonTest.kt | 40 + .../SubscriptionBillingIssueAmazonTest.kt | 52 ++ packages/google/package.json | 2 +- packages/gql/codegen/core/schema-linter.ts | 69 +- packages/gql/src/generated/Types.cs | 30 +- packages/gql/src/generated/Types.kt | 54 +- packages/gql/src/generated/Types.swift | 31 +- packages/gql/src/generated/types.dart | 48 +- packages/gql/src/generated/types.gd | 53 +- packages/gql/src/generated/types.ts | 20 +- packages/gql/src/type.graphql | 29 +- packages/kit/convex/_generated/api.d.ts | 2 + packages/kit/convex/analytics/action.ts | 1 + packages/kit/convex/projects/mutation.ts | 27 + packages/kit/convex/projects/query.ts | 19 +- packages/kit/convex/projects/setupStatus.ts | 11 + packages/kit/convex/purchases/action.ts | 2 + packages/kit/convex/purchases/amazon.test.ts | 91 ++ packages/kit/convex/purchases/amazon.ts | 267 ++++++ packages/kit/convex/purchases/errors.ts | 29 + .../purchases/extract-product-id.test.ts | 29 + packages/kit/convex/purchases/shared.ts | 12 +- packages/kit/convex/schema.ts | 17 + .../kit/server/api/v1/replay-guard.test.ts | 24 + packages/kit/server/api/v1/replay-guard.ts | 29 +- .../kit/server/api/v1/request-logger.test.ts | 25 + packages/kit/server/api/v1/request-logger.ts | 2 +- .../server/api/v1/route-input-schemas.test.ts | 63 ++ .../kit/server/api/v1/route-input-schemas.ts | 63 +- packages/kit/server/api/v1/routes.ts | 30 +- .../organization/project/PurchasesTable.tsx | 18 +- .../auth/organization/project/analytics.tsx | 5 +- .../auth/organization/project/products.tsx | 5 +- .../organization/project/purchase-detail.tsx | 6 + .../auth/organization/project/purchases.tsx | 18 +- .../auth/organization/project/settings.tsx | 131 +++ .../organization/project/subscriptions.tsx | 5 +- .../auth/organization/project/webhooks.tsx | 13 +- .../src/pages/blog/iapkit-joins-openiap.tsx | 10 +- packages/kit/src/pages/docs/sections/api.tsx | 23 +- .../src/pages/docs/sections/quickstart.tsx | 12 + scripts/agent/compile-context.ts | 181 ++-- 144 files changed, 8294 insertions(+), 750 deletions(-) create mode 100644 libraries/expo-iap/src/__tests__/native-log-redaction.test.js create mode 100644 libraries/expo-iap/src/__tests__/vega-adapter.test.ts create mode 100644 libraries/expo-iap/src/amazon-devices-kepler.d.ts create mode 100644 libraries/expo-iap/src/vega-adapter.ts create mode 100644 libraries/expo-iap/src/vega.kepler.ts create mode 100644 libraries/expo-iap/src/vega.ts create mode 100644 libraries/react-native-iap/src/__tests__/vega-adapter.test.ts create mode 100644 libraries/react-native-iap/src/types/amazon-devices-kepler/index.d.ts create mode 100644 libraries/react-native-iap/src/vega-adapter.ts create mode 100644 libraries/react-native-iap/src/vega.kepler.ts create mode 100644 libraries/react-native-iap/src/vega.ts create mode 100644 packages/docs/src/pages/docs/features/runtime-integrations.tsx create mode 100644 packages/docs/src/pages/docs/features/vega-os.tsx create mode 100644 packages/docs/src/pages/docs/fireos-setup.tsx create mode 100644 packages/google/openiap/src/amazon/AndroidManifest.xml create mode 100644 packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapErrorExtensions.kt create mode 100644 packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt create mode 100644 packages/google/openiap/src/amazon/java/dev/hyo/openiap/store/OpenIapStoreExtensions.kt create mode 100644 packages/google/openiap/src/testAmazon/java/dev/hyo/openiap/FetchProductsAmazonTest.kt create mode 100644 packages/google/openiap/src/testAmazon/java/dev/hyo/openiap/SubscriptionBillingIssueAmazonTest.kt create mode 100644 packages/kit/convex/purchases/amazon.test.ts create mode 100644 packages/kit/convex/purchases/amazon.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e633774a..77e7661c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -198,6 +198,10 @@ jobs: working-directory: packages/google run: ./gradlew :openiap:assembleHorizonDebug + - name: Build Fire OS flavor + working-directory: packages/google + run: ./gradlew :openiap:assembleAmazonDebug + test-ios: name: Test iOS runs-on: macos-15 diff --git a/.github/workflows/release-google.yml b/.github/workflows/release-google.yml index 04d3445a..0b900c50 100644 --- a/.github/workflows/release-google.yml +++ b/.github/workflows/release-google.yml @@ -290,6 +290,38 @@ jobs: echo "✅ Published openiap-google-horizon (Horizon flavor) to Maven Central" fi + - name: Check if Fire OS flavor already published + id: check_amazon + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + if curl -s "https://repo1.maven.org/maven2/io/github/hyochan/openiap/openiap-google-amazon/$VERSION/" | grep -q "$VERSION"; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "⚠️ openiap-google-amazon $VERSION already exists on Maven Central" + else + echo "exists=false" >> $GITHUB_OUTPUT + echo "✓ openiap-google-amazon $VERSION does not exist, will publish" + fi + + - name: Publish Fire OS flavor to Maven Central + if: steps.check_amazon.outputs.exists == 'false' + working-directory: packages/google + env: + ORG_GRADLE_PROJECT_openIapVersion: ${{ steps.version.outputs.version }} + ORG_GRADLE_PROJECT_OPENIAP_PUBLISH_VARIANT: amazon + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY_CONTENTS }} + ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} + run: | + if [ -z "$ORG_GRADLE_PROJECT_mavenCentralUsername" ]; then + echo "⚠️ Maven Central credentials not set. Skipping publish." + else + ./gradlew :openiap:publishAndReleaseToMavenCentral --no-daemon --no-parallel --stacktrace + echo "✅ Published openiap-google-amazon (Fire OS flavor) to Maven Central" + fi + - name: Check if Play flavor already published id: check_play env: @@ -381,7 +413,7 @@ jobs: cat > /tmp/release-notes.md < **Auto-generated for Claude Code** -> Last updated: 2026-05-16T12:59:43.317Z +> Last updated: 2026-05-18T04:52:45.916Z > > Usage: `claude --context knowledge/_claude-context/context.md` @@ -1759,7 +1759,7 @@ Use these checks before writing a release list: If the release is not published yet, use planned wording and plain text. If the release is published, verify the tag exists with `gh release view ` before linking it. This prevents stale Package Releases tables such as documenting -`maui-iap 1.0.1` when the actual release tag is `maui-iap-1.0.2`. +`maui-iap 1.0.1` when the actual release tag is `maui-iap-1.0.3`. --- @@ -1864,6 +1864,7 @@ react-native-iap / godot-iap, then the Apple wrapper must also default to description is the canonical statement. When changing a default, update: + 1. The GraphQL schema description. 2. Re-run `bun run generate`. 3. Every wrapper SDK's `?? ` expression and JSDoc / KDoc / etc. @@ -1878,6 +1879,7 @@ The audit script greps for fields that don't appear in the type definition and flags them. Example failure modes already encountered: + - `BillingProgramAvailabilityResultAndroid` doc listed `responseCode` + `debugMessage` — neither field exists; the type has `billingProgram` + `isAvailable`. @@ -1903,6 +1905,7 @@ the union is `'browser'` only, but the doc claimed Anchor links should point to existing pages and section anchors. Common recent failures: + - "Use verifyPurchase" link pointed to `/docs/apis/get-active-subscriptions` (totally unrelated). - `getExternalPurchaseCustomLinkTokenIOS` Returns linked to the @@ -1930,6 +1933,7 @@ exactly as Google / Apple states it. Code examples in doc pages should at minimum parse / type-check against the wrapper they target. The audit script does NOT yet run a full TypeScript / Kotlin / Dart parser, but it does: + - Verify imports (`import {…} from 'expo-iap'`) reference symbols that expo-iap actually exports. - Verify field accesses on shown objects (e.g. `purchase.purchaseToken`) @@ -1961,6 +1965,27 @@ the GitHub Release does not exist yet. `bun run audit:docs` fails bare package/version entries under published `Package Releases` blocks so link regressions are caught before publishing. +### R10 — Docs version metadata stays synced with package metadata + +`packages/docs/src/lib/versioning.ts` must not import package metadata from +outside `packages/docs`. Vercel uploads the docs package root, so imports such +as `../../../../libraries/expo-iap/package.json?raw` pass locally but fail in +Vercel builds. + +Framework package versions and Android SDK constants used by docs must flow +through `packages/docs/src/generated/version-metadata.json`, which is generated +by `scripts/sync-versions.sh` from the real SSOT files: + +- Expo / React Native: each library `package.json` +- Flutter: `libraries/flutter_inapp_purchase/pubspec.yaml` +- Godot: `libraries/godot-iap/addons/godot-iap/plugin.cfg` +- KMP: `libraries/kmp-iap/gradle.properties` and `gradle/libs.versions.toml` +- MAUI: `libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj` +- Google Android SDK / Play Billing: `packages/google/openiap/build.gradle.kts` + +`bun run audit:docs` fails if this generated metadata drifts from the SSOT +files or if `versioning.ts` reintroduces raw imports outside `packages/docs`. + ## Pre-commit checklist Run before every `git push` on docs / SDK changes: @@ -1990,6 +2015,7 @@ positives in CI. `scripts/audit-docs.ts` is the executable companion to this guide. It parses every `/docs/apis/*.tsx` and `/docs/types/*.tsx` page, extracts: + - `` targets - `fieldName` mentions inside Returns / Parameters tables - String-literal enum values in `'…'` blocks diff --git a/knowledge/internal/03-coding-style.md b/knowledge/internal/03-coding-style.md index cff0e053..e2b3c238 100644 --- a/knowledge/internal/03-coding-style.md +++ b/knowledge/internal/03-coding-style.md @@ -89,6 +89,35 @@ let userId = '123'; var config = { timeout: 5000 }; ``` +### Keep Single-Use Helpers Local + +Private helper functions used by only one function should be declared inside +that function so their scope matches their real ownership. Keep helpers at file +scope only when they are exported, reused by multiple call sites, or need a +stable top-level identity for tests, recursion, or platform registration. + +```typescript +// ✅ CORRECT - helper is owned by getResolved() +function getResolved(): ResolvedModule { + function getExpectedModuleName(): NativeModuleName { + return isVegaOS() ? 'ExpoIapVega' : 'ExpoIap'; + } + + const expectedName = getExpectedModuleName(); + return resolve(expectedName); +} + +// ❌ INCORRECT - helper has only one call site but lives at file scope +function getExpectedModuleName(): NativeModuleName { + return isVegaOS() ? 'ExpoIapVega' : 'ExpoIap'; +} + +function getResolved(): ResolvedModule { + const expectedName = getExpectedModuleName(); + return resolve(expectedName); +} +``` + ### Prefer Interface Over Type for Objects ```typescript diff --git a/knowledge/internal/06-git-deployment.md b/knowledge/internal/06-git-deployment.md index d10ea6b0..652b5dae 100644 --- a/knowledge/internal/06-git-deployment.md +++ b/knowledge/internal/06-git-deployment.md @@ -186,7 +186,7 @@ Use these checks before writing a release list: If the release is not published yet, use planned wording and plain text. If the release is published, verify the tag exists with `gh release view ` before linking it. This prevents stale Package Releases tables such as documenting -`maui-iap 1.0.1` when the actual release tag is `maui-iap-1.0.2`. +`maui-iap 1.0.1` when the actual release tag is `maui-iap-1.0.3`. --- diff --git a/libraries/expo-iap/.npmignore b/libraries/expo-iap/.npmignore index 893cee37..74184c52 100644 --- a/libraries/expo-iap/.npmignore +++ b/libraries/expo-iap/.npmignore @@ -13,3 +13,7 @@ __tests__ /android/build/ /example/ /docs/ +/coverage/ +/.nyc_output/ +*.lcov +*.tsbuildinfo diff --git a/libraries/expo-iap/android/build.gradle b/libraries/expo-iap/android/build.gradle index e4fd012b..4050e94c 100644 --- a/libraries/expo-iap/android/build.gradle +++ b/libraries/expo-iap/android/build.gradle @@ -50,6 +50,13 @@ if (!(googleVersion instanceof String) || !googleVersion.trim()) { def googleVersionString = googleVersion.trim() apply from: project.file('openiap-android-sdk.gradle') +def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false +def fireOsEnabled = project.findProperty('fireOsEnabled')?.toBoolean() ?: false +if (horizonEnabled && fireOsEnabled) { + throw new GradleException("expo-iap: horizonEnabled and fireOsEnabled cannot both be true") +} +def openiapFlavor = fireOsEnabled ? 'amazon' : (horizonEnabled ? 'horizon' : 'play') + // If you want to use the managed Android SDK versions from expo-modules-core, set this to true. // The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code. // Most of the time, you may like to manage the Android SDK versions yourself. @@ -76,17 +83,14 @@ android { versionCode = 1 versionName = expoIapPackageVersion // When using local openiap-google with flavors, select the appropriate flavor - // Read horizonEnabled from gradle.properties, default to play - def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false - def flavor = horizonEnabled ? 'horizon' : 'play' - missingDimensionStrategy "platform", flavor + missingDimensionStrategy "platform", openiapFlavor } - lintOptions { + lint { abortOnError = false } compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } @@ -100,12 +104,12 @@ kotlin { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" - // Determine which OpenIAP dependency to use based on horizonEnabled flag - def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false - // Use OpenIAP Google module only; avoid direct BillingClient dependency if (findProject(":openiap-google") != null) { implementation project(":openiap-google") + } else if (fireOsEnabled) { + // Use openiap-google-amazon for Fire OS when fireOsEnabled is true + implementation "io.github.hyochan.openiap:openiap-google-amazon:${googleVersionString}" } else if (horizonEnabled) { // Use openiap-google-horizon for Meta Quest when horizonEnabled is true implementation "io.github.hyochan.openiap:openiap-google-horizon:${googleVersionString}" diff --git a/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapLog.kt b/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapLog.kt index 4c124c2b..9180039a 100644 --- a/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapLog.kt +++ b/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapLog.kt @@ -64,7 +64,7 @@ internal object ExpoIapLog { val sanitized = linkedMapOf() for ((rawKey, rawValue) in source) { val key = rawKey as? String ?: continue - if (key.lowercase().contains("token")) { + if (isSensitiveKey(key)) { sanitized[key] = "hidden" continue } @@ -72,4 +72,11 @@ internal object ExpoIapLog { } return sanitized } + + private fun isSensitiveKey(key: String): Boolean { + val normalized = key.lowercase().filter { it.isLetterOrDigit() } + return listOf("token", "apikey", "secret", "jws", "receiptid", "userid").any { + normalized.contains(it) + } + } } diff --git a/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapModule.kt b/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapModule.kt index 2f08fb94..4639a0f2 100644 --- a/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +++ b/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapModule.kt @@ -3,6 +3,7 @@ package expo.modules.iap import android.content.Context import dev.hyo.openiap.AndroidSubscriptionOfferInput import dev.hyo.openiap.DeepLinkOptions +import dev.hyo.openiap.FetchProductsResultAll import dev.hyo.openiap.FetchProductsResultProducts import dev.hyo.openiap.FetchProductsResultSubscriptions import dev.hyo.openiap.InitConnectionConfig @@ -183,9 +184,9 @@ class ExpoIapModule : Module() { val result = openIap.fetchProducts(request) val payload = when (result) { + is FetchProductsResultAll -> result.value.orEmpty().map { it.toJson() } is FetchProductsResultProducts -> result.value.orEmpty().map { it.toJson() } is FetchProductsResultSubscriptions -> result.value.orEmpty().map { it.toJson() } - else -> emptyList>() } ExpoIapLog.result("fetchProducts", payload) promise.resolve(payload) @@ -495,7 +496,13 @@ class ExpoIapModule : Module() { } AsyncFunction("verifyPurchaseWithProvider") { params: Map, promise: Promise -> - ExpoIapLog.payload("verifyPurchaseWithProvider", params) + ExpoIapLog.payload( + "verifyPurchaseWithProvider", + mapOf( + "provider" to params["provider"], + "hasIapkit" to (params["iapkit"] != null), + ), + ) scope.launch { try { val props = diff --git a/libraries/expo-iap/bun.lock b/libraries/expo-iap/bun.lock index 7488173d..50926c28 100644 --- a/libraries/expo-iap/bun.lock +++ b/libraries/expo-iap/bun.lock @@ -20,10 +20,16 @@ "ts-jest": "^29.4.1", }, "peerDependencies": { + "@amazon-devices/keplerscript-appstore-iap-lib": "~2.12.13", + "@amazon-devices/package-manager-lib": "~1.0.1767254401", "expo": "*", "react": "*", "react-native": "*", }, + "optionalPeers": [ + "@amazon-devices/keplerscript-appstore-iap-lib", + "@amazon-devices/package-manager-lib", + ], }, }, "packages": { diff --git a/libraries/expo-iap/example/app.config.ts b/libraries/expo-iap/example/app.config.ts index 5e731708..fc0b8c76 100644 --- a/libraries/expo-iap/example/app.config.ts +++ b/libraries/expo-iap/example/app.config.ts @@ -10,11 +10,15 @@ const LOCAL_OPENIAP_PATHS = { // Read library version mode from libraries-versions.jsonc const parseJsonc = (text: string) => JSON.parse(text.replace(/^\s*\/\/.*$/gm, '')); -const versionsPath = path.resolve(__dirname, '../../../libraries-versions.jsonc'); +const versionsPath = path.resolve( + __dirname, + '../../../libraries-versions.jsonc', +); const librariesVersions: Record = fs.existsSync(versionsPath) ? parseJsonc(fs.readFileSync(versionsPath, 'utf8')) : {'expo-iap': 'local'}; -const useLocalDev = !librariesVersions['expo-iap'] || librariesVersions['expo-iap'] === 'local'; +const useLocalDev = + !librariesVersions['expo-iap'] || librariesVersions['expo-iap'] === 'local'; export default ({config}: ConfigContext): ExpoConfig => { // Check if building for TV (set EXPO_TV=1 before prebuild) @@ -42,6 +46,8 @@ export default ({config}: ConfigContext): ExpoConfig => { onside: isOnsideEnabled, // Horizon module: Android only (Meta Quest/VR devices) horizon: false, + // Fire OS module: Android only + fireOS: false, }, android: { // Horizon App ID for Meta Quest/VR devices (required when modules.horizon is true) diff --git a/libraries/expo-iap/ios/ExpoIapLog.swift b/libraries/expo-iap/ios/ExpoIapLog.swift index 1a63bc51..7a4260eb 100644 --- a/libraries/expo-iap/ios/ExpoIapLog.swift +++ b/libraries/expo-iap/ios/ExpoIapLog.swift @@ -116,7 +116,7 @@ enum ExpoIapLog { private static func sanitizeDictionary(_ dictionary: [String: Any]) -> [String: Any] { var sanitized: [String: Any] = [:] for (key, value) in dictionary { - if key.lowercased().contains("token") { + if isSensitiveKey(key) { sanitized[key] = "hidden" } else if let sanitizedValue = sanitize(value) { sanitized[key] = sanitizedValue @@ -124,4 +124,11 @@ enum ExpoIapLog { } return sanitized } + + private static func isSensitiveKey(_ key: String) -> Bool { + let normalized = key.lowercased() + .filter { $0.isLetter || $0.isNumber } + let sensitiveFragments = ["token", "apikey", "secret", "jws", "receiptid", "userid"] + return sensitiveFragments.contains { normalized.contains($0) } + } } diff --git a/libraries/expo-iap/ios/ExpoIapModule.swift b/libraries/expo-iap/ios/ExpoIapModule.swift index c6e7a2ec..cbb75665 100644 --- a/libraries/expo-iap/ios/ExpoIapModule.swift +++ b/libraries/expo-iap/ios/ExpoIapModule.swift @@ -240,7 +240,13 @@ public final class ExpoIapModule: Module { } AsyncFunction("verifyPurchaseWithProvider") { (params: [String: Any]) async throws -> [String: Any] in - ExpoIapLog.payload("verifyPurchaseWithProvider", payload: params) + ExpoIapLog.payload( + "verifyPurchaseWithProvider", + payload: [ + "provider": params["provider"] ?? "unknown", + "hasIapkit": params["iapkit"] != nil + ] + ) do { let jsonData = try JSONSerialization.data(withJSONObject: params) let props = try JSONDecoder().decode(VerifyPurchaseWithProviderProps.self, from: jsonData) diff --git a/libraries/expo-iap/package.json b/libraries/expo-iap/package.json index 028576e8..a47e89b0 100644 --- a/libraries/expo-iap/package.json +++ b/libraries/expo-iap/package.json @@ -1,6 +1,6 @@ { "name": "expo-iap", - "version": "4.3.1", + "version": "4.4.0-rc.3", "description": "In App Purchase module in Expo", "main": "build/index.js", "types": "build/index.d.ts", @@ -65,10 +65,20 @@ "ts-jest": "^29.4.1" }, "peerDependencies": { + "@amazon-devices/keplerscript-appstore-iap-lib": "~2.12.13", + "@amazon-devices/package-manager-lib": "~1.0.1767254401", "expo": "*", "react": "*", "react-native": "*" }, + "peerDependenciesMeta": { + "@amazon-devices/keplerscript-appstore-iap-lib": { + "optional": true + }, + "@amazon-devices/package-manager-lib": { + "optional": true + } + }, "expo": { "plugin": "./app.plugin.js" } diff --git a/libraries/expo-iap/plugin/__tests__/withIAP.test.ts b/libraries/expo-iap/plugin/__tests__/withIAP.test.ts index 65166117..204e5ae9 100644 --- a/libraries/expo-iap/plugin/__tests__/withIAP.test.ts +++ b/libraries/expo-iap/plugin/__tests__/withIAP.test.ts @@ -1,16 +1,18 @@ import type {ExpoConfig} from '@expo/config-types'; -import { +import plugin, { computeAutolinkModules, ensureOnsidePodIOS, modifyAppBuildGradle, resolveModuleSelection, + syncHorizonAppIdMetaData, } from '../src/withIAP'; +import {getAndroidLocalPathInput} from '../src/withLocalOpenIAP'; import type {AutolinkState} from '../src/withIAP'; import type {ExpoIapPluginCommonOptions} from '../src/expoConfig.augmentation'; // Type-level expectations const autoModeOptions: ExpoIapPluginCommonOptions = { - modules: {onside: true}, + modules: {onside: true, fireOS: false, vega: false}, }; const explicitModeOptions: ExpoIapPluginCommonOptions = { @@ -57,11 +59,151 @@ describe('android configuration', () => { expect(matches).toHaveLength(1); expect(result).not.toContain('openiap-google:0.0.1'); }); + + it('uses Fire OS artifact and flavor when Fire OS is enabled', () => { + const baseGradle = [ + 'android {', + ' defaultConfig {', + ' }', + '}', + 'dependencies {', + ' implementation "io.github.hyochan.openiap:openiap-google-horizon:0.0.1"', + '}', + '', + ].join('\n'); + const result = modifyAppBuildGradle(baseGradle, 'groovy', false, true); + + expect(result).toContain( + ` implementation "io.github.hyochan.openiap:openiap-google-amazon:${dependencyVersion}"`, + ); + expect(result).toContain( + ' missingDimensionStrategy "platform", "amazon"', + ); + expect(result).not.toContain('openiap-google-horizon:0.0.1'); + }); + + it('prefers Fire OS over Horizon when both store flags are enabled', () => { + const baseGradle = [ + 'android {', + ' defaultConfig {', + ' }', + '}', + 'dependencies {', + '}', + '', + ].join('\n'); + const result = modifyAppBuildGradle(baseGradle, 'kotlin', true, true); + + expect(result).toContain( + ` implementation("io.github.hyochan.openiap:openiap-google-amazon:${dependencyVersion}")`, + ); + expect(result).toContain( + ' missingDimensionStrategy("platform", "amazon")', + ); + }); + + it('replaces stale platform strategy when returning to Play', () => { + const baseGradle = [ + 'android {', + ' defaultConfig {', + ' missingDimensionStrategy "platform", "amazon"', + ' }', + '}', + 'dependencies {', + ' implementation "io.github.hyochan.openiap:openiap-google-amazon:0.0.1"', + '}', + '', + ].join('\n'); + const result = modifyAppBuildGradle(baseGradle, 'groovy'); + + expect(result).toContain( + ` implementation "io.github.hyochan.openiap:openiap-google:${dependencyVersion}"`, + ); + expect(result).not.toContain('openiap-google-amazon:0.0.1'); + expect(result).toContain('missingDimensionStrategy "platform", "play"'); + }); + + it('rejects Vega OS combined with Fire OS during prebuild', () => { + expect(() => + plugin({name: 'test-app', slug: 'test-app'} as ExpoConfig, { + modules: {fireOS: true, vega: true}, + }), + ).toThrow(/modules\.vega cannot be combined/); + }); + + it('removes Horizon App ID metadata outside Horizon builds', () => { + const manifest = { + manifest: { + application: [ + { + 'meta-data': [ + { + $: { + 'android:name': 'com.meta.horizon.platform.ovr.OCULUS_APP_ID', + 'android:value': '123', + }, + }, + { + $: { + 'android:name': 'dev.iapkit.API_KEY', + 'android:value': 'key', + }, + }, + ], + }, + ], + }, + }; + + expect(syncHorizonAppIdMetaData(manifest, false, '123')).toBe('removed'); + expect(manifest.manifest.application[0]!['meta-data']).toEqual([ + { + $: { + 'android:name': 'dev.iapkit.API_KEY', + 'android:value': 'key', + }, + }, + ]); + }); + + it('adds Horizon App ID metadata only for Horizon builds', () => { + const manifest = {manifest: {}}; + + expect(syncHorizonAppIdMetaData(manifest, false, '123')).toBe('unchanged'); + expect(manifest.manifest).not.toHaveProperty('application'); + + expect(syncHorizonAppIdMetaData(manifest, true, '123')).toBe('added'); + expect(manifest.manifest.application?.[0]?.['meta-data']).toEqual([ + { + $: { + 'android:name': 'com.meta.horizon.platform.ovr.OCULUS_APP_ID', + 'android:value': '123', + }, + }, + ]); + }); +}); + +describe('local OpenIAP configuration', () => { + it('uses string localPath for Android local module resolution', () => { + expect(getAndroidLocalPathInput('/repo/packages/google')).toBe( + '/repo/packages/google', + ); + }); + + it('uses android localPath when platform paths are split', () => { + expect( + getAndroidLocalPathInput({ + ios: '/repo/packages/apple', + android: '/repo/packages/google', + }), + ).toBe('/repo/packages/google'); + }); }); describe('ios module selection', () => { const createConfig = (ios?: ExpoConfig['ios']): ExpoConfig => - ({name: 'test-app', slug: 'test-app', ios} as ExpoConfig); + ({name: 'test-app', slug: 'test-app', ios}) as ExpoConfig; it('defaults to Expo IAP only when no options provided', () => { const result = resolveModuleSelection(createConfig(), undefined); diff --git a/libraries/expo-iap/plugin/src/expoConfig.augmentation.d.ts b/libraries/expo-iap/plugin/src/expoConfig.augmentation.d.ts index 28eb6132..69475311 100644 --- a/libraries/expo-iap/plugin/src/expoConfig.augmentation.d.ts +++ b/libraries/expo-iap/plugin/src/expoConfig.augmentation.d.ts @@ -14,6 +14,18 @@ export type ExpoIapModuleOverrides = { * @default false */ horizon?: boolean; + /** + * Enable Fire OS support for Amazon-distributed Android builds + * @platform android + * @default false + */ + fireOS?: boolean; + /** + * Mark this config as targeting Vega OS. Vega OS is selected by the kepler + * runtime and must not be combined with Android store flavors. + * @default false + */ + vega?: boolean; }; type BaseExpoIapOptions = { @@ -29,23 +41,23 @@ type BaseExpoIapOptions = { * Configure external purchase countries, links, and entitlements. * Requires approval from Apple. * @platform ios - */ - iosAlternativeBilling?: IOSAlternativeBillingConfig; - /** * @deprecated Use ios.alternativeBilling instead */ + iosAlternativeBilling?: IOSAlternativeBillingConfig; ios?: { alternativeBilling?: IOSAlternativeBillingConfig; }; /** * Horizon OS app ID for Quest devices * @platform android + * @deprecated Use android.horizonAppId instead */ horizonAppId?: string; - /** - * @deprecated Use modules.horizon and android.horizonAppId instead - */ android?: { + /** + * Horizon OS app ID for Quest devices + * @platform android + */ horizonAppId?: string; }; }; diff --git a/libraries/expo-iap/plugin/src/withIAP.ts b/libraries/expo-iap/plugin/src/withIAP.ts index 6bc2c71d..e0b23ac7 100644 --- a/libraries/expo-iap/plugin/src/withIAP.ts +++ b/libraries/expo-iap/plugin/src/withIAP.ts @@ -19,18 +19,33 @@ import { import type {ExpoIapPluginCommonOptions} from './expoConfig.augmentation'; const pkg = require('../../package.json'); -const openiapVersions = JSON.parse( - fs.readFileSync( - path.resolve(__dirname, '../../openiap-versions.json'), - 'utf8', - ), -); -const OPENIAP_ANDROID_VERSION = openiapVersions.google; const AUTOLINKING_CONFIG_PATH = path.resolve( __dirname, '../../expo-module.config.json', ); +function loadOpenIapAndroidVersion(): string { + const versionsPath = path.resolve(__dirname, '../../openiap-versions.json'); + try { + const raw = fs.readFileSync(versionsPath, 'utf8'); + const parsed = JSON.parse(raw); + const googleVersion = + typeof parsed?.google === 'string' ? parsed.google.trim() : ''; + if (!googleVersion) { + throw new Error( + 'expo-iap: "google" version missing or invalid in openiap-versions.json', + ); + } + return googleVersion; + } catch (error) { + throw new Error( + `expo-iap: Unable to load openiap-versions.json (${ + error instanceof Error ? error.message : error + })`, + ); + } +} + // Log a message only once per Node process const logOnce = (() => { const printed = new Set(); @@ -62,20 +77,107 @@ const addLineToGradle = ( return lines.join('\n'); }; +const HORIZON_APP_ID_META_DATA_NAME = + 'com.meta.horizon.platform.ovr.OCULUS_APP_ID'; + +type AndroidManifestLike = { + manifest: { + application?: Array>; + }; +}; + +type HorizonAppIdSyncResult = 'added' | 'updated' | 'removed' | 'unchanged'; + +export function syncHorizonAppIdMetaData( + manifest: AndroidManifestLike, + isHorizonEnabled?: boolean, + horizonAppId?: string, +): HorizonAppIdSyncResult { + const application = manifest.manifest.application?.[0]; + const existingMetaData = application?.['meta-data']; + + if (!isHorizonEnabled) { + if (!Array.isArray(existingMetaData)) return 'unchanged'; + + const nextMetaData = existingMetaData.filter( + (m) => m.$?.['android:name'] !== HORIZON_APP_ID_META_DATA_NAME, + ); + if (nextMetaData.length === existingMetaData.length || !application) { + return 'unchanged'; + } + + application['meta-data'] = nextMetaData; + return 'removed'; + } + + if (!horizonAppId) return 'unchanged'; + + if ( + !manifest.manifest.application || + manifest.manifest.application.length === 0 + ) { + manifest.manifest.application = [{$: {'android:name': '.MainApplication'}}]; + } + + const horizonApplication = manifest.manifest.application[0]!; + if (!horizonApplication['meta-data']) { + horizonApplication['meta-data'] = []; + } + + const metaData = horizonApplication['meta-data']; + const horizonAppIdMeta = { + $: { + 'android:name': HORIZON_APP_ID_META_DATA_NAME, + 'android:value': horizonAppId, + }, + }; + + const existingIndex = metaData.findIndex( + (m: any) => m.$?.['android:name'] === HORIZON_APP_ID_META_DATA_NAME, + ); + + if (existingIndex !== -1) { + metaData[existingIndex] = horizonAppIdMeta; + return 'updated'; + } + + metaData.push(horizonAppIdMeta); + return 'added'; +} + export const modifyAppBuildGradle = ( gradle: string, language: 'groovy' | 'kotlin', isHorizonEnabled?: boolean, + isFireOsEnabled?: boolean, ): string => { let modified = gradle; - // Determine which flavor to use based on isHorizonEnabled - const flavor = isHorizonEnabled ? 'horizon' : 'play'; + let openIapAndroidVersion: string; + try { + openIapAndroidVersion = loadOpenIapAndroidVersion(); + } catch (error) { + WarningAggregator.addWarningAndroid( + 'expo-iap', + `expo-iap: Failed to resolve OpenIAP version (${ + error instanceof Error ? error.message : error + })`, + ); + return gradle; + } - // Use openiap-google-horizon artifact when horizon is enabled - const artifactId = isHorizonEnabled - ? 'openiap-google-horizon' - : 'openiap-google'; + // Determine which flavor to use based on store flags. + const flavor = isFireOsEnabled + ? 'amazon' + : isHorizonEnabled + ? 'horizon' + : 'play'; + + const artifactId = isFireOsEnabled + ? 'openiap-google-amazon' + : isHorizonEnabled + ? 'openiap-google-horizon' + : 'openiap-google'; // Ensure OpenIAP dependency exists at desired version in app-level build.gradle(.kts) const impl = (ga: string, v: string) => @@ -84,12 +186,12 @@ export const modifyAppBuildGradle = ( : ` implementation "${ga}:${v}"`; const openiapDep = impl( `io.github.hyochan.openiap:${artifactId}`, - OPENIAP_ANDROID_VERSION, + openIapAndroidVersion, ); - // Remove any existing openiap-google or openiap-google-horizon lines (any version, groovy/kotlin, implementation/api) + // Remove any existing openiap-google flavor lines (any version, groovy/kotlin, implementation/api) const openiapAnyLine = - /^\s*(?:implementation|api)\s*\(?\s*["']io\.github\.hyochan\.openiap:openiap-google(?:-horizon)?:[^"']+["']\s*\)?\s*$/gm; + /^\s*(?:implementation|api)\s*\(?\s*["']io\.github\.hyochan\.openiap:openiap-google(?:-(?:horizon|amazon))?:[^"']+["']\s*\)?\s*$/gm; const hadExisting = openiapAnyLine.test(modified); if (hadExisting) { modified = modified.replace(openiapAnyLine, '').replace(/\n{3,}/g, '\n\n'); @@ -98,48 +200,41 @@ export const modifyAppBuildGradle = ( // Ensure the desired dependency line is present if ( !new RegExp( - String.raw`io\.github\.hyochan\.openiap:${artifactId}:${OPENIAP_ANDROID_VERSION}`, + String.raw`io\.github\.hyochan\.openiap:${artifactId}:${openIapAndroidVersion}`, ).test(modified) ) { // Insert just after the opening `dependencies {` line modified = addLineToGradle(modified, /dependencies\s*{/, openiapDep, 1); logOnce( hadExisting - ? `🛠️ expo-iap: Replaced OpenIAP dependency with ${OPENIAP_ANDROID_VERSION}` - : `🛠️ expo-iap: Added OpenIAP dependency (${OPENIAP_ANDROID_VERSION}) to build.gradle`, + ? `🛠️ expo-iap: Replaced OpenIAP dependency with ${openIapAndroidVersion}` + : `🛠️ expo-iap: Added OpenIAP dependency (${openIapAndroidVersion}) to build.gradle`, ); } - // Add flavor dimension and default config for OpenIAP if horizon is enabled - if (isHorizonEnabled) { - // Add missingDimensionStrategy to select horizon flavor - const defaultConfigRegex = /defaultConfig\s*{/; - if (defaultConfigRegex.test(modified)) { - const strategyLine = - language === 'kotlin' - ? ` missingDimensionStrategy("platform", "${flavor}")` - : ` missingDimensionStrategy "platform", "${flavor}"`; - - // Remove any existing platform strategies first to avoid duplicates - const strategyPattern = - /^\s*missingDimensionStrategy\s*\(?\s*["']platform["']\s*,\s*["'](play|horizon)["']\s*\)?\s*$/gm; - if (strategyPattern.test(modified)) { - modified = modified.replace(strategyPattern, ''); - logOnce('🧹 Removed existing missingDimensionStrategy for platform'); - } + // Remove stale OpenIAP platform strategies even when returning to the default + // Play artifact. Otherwise a previous Fire OS/Horizon prebuild can keep + // selecting the wrong local flavor. + const strategyPattern = + /^\s*missingDimensionStrategy\s*\(?\s*["']platform["']\s*,\s*["'](play|horizon|amazon)["']\s*\)?\s*$/gm; + if (strategyPattern.test(modified)) { + modified = modified.replace(strategyPattern, ''); + logOnce('🧹 Removed existing missingDimensionStrategy for platform'); + } - // Add the new strategy - if (!/missingDimensionStrategy.*platform/.test(modified)) { - modified = addLineToGradle( - modified, - defaultConfigRegex, - strategyLine, - 1, - ); - logOnce( - `🛠️ expo-iap: Added missingDimensionStrategy for ${flavor} flavor`, - ); - } + const defaultConfigRegex = /defaultConfig\s*{/; + if (defaultConfigRegex.test(modified)) { + const strategyLine = + language === 'kotlin' + ? ` missingDimensionStrategy("platform", "${flavor}")` + : ` missingDimensionStrategy "platform", "${flavor}"`; + + // Add the new strategy + if (!/missingDimensionStrategy.*platform/.test(modified)) { + modified = addLineToGradle(modified, defaultConfigRegex, strategyLine, 1); + logOnce( + `🛠️ expo-iap: Added missingDimensionStrategy for ${flavor} flavor`, + ); } } @@ -151,6 +246,7 @@ const withIapAndroid: ConfigPlugin< addDeps?: boolean; horizonAppId?: string; isHorizonEnabled?: boolean; + isFireOsEnabled?: boolean; } | void > = (config, props) => { const addDeps = props?.addDeps ?? true; @@ -163,28 +259,36 @@ const withIapAndroid: ConfigPlugin< config.modResults.contents, language, props?.isHorizonEnabled, + props?.isFireOsEnabled, ); return config; }); } - // Set horizonEnabled property in gradle.properties so expo-iap module can pick it up + // Set store flags in gradle.properties so expo-iap module can pick them up. config = withGradleProperties(config, (config) => { const horizonValue = props?.isHorizonEnabled ?? false; + const fireOsValue = props?.isFireOsEnabled ?? false; - // Remove any existing horizonEnabled entries config.modResults = config.modResults.filter( - (item) => item.type !== 'property' || item.key !== 'horizonEnabled', + (item) => + item.type !== 'property' || + !['horizonEnabled', 'fireOsEnabled'].includes(item.key), ); - // Add the horizonEnabled property config.modResults.push({ type: 'property', key: 'horizonEnabled', value: String(horizonValue), }); + config.modResults.push({ + type: 'property', + key: 'fireOsEnabled', + value: String(fireOsValue), + }); logOnce(`✅ Set horizonEnabled=${horizonValue} in gradle.properties`); + logOnce(`✅ Set fireOsEnabled=${fireOsValue} in gradle.properties`); return config; }); @@ -200,62 +304,49 @@ const withIapAndroid: ConfigPlugin< const permissions = manifest.manifest['uses-permission']; const billingPerm = {$: {'android:name': 'com.android.vending.BILLING'}}; - const alreadyExists = permissions.some( - (p) => p.$['android:name'] === 'com.android.vending.BILLING', - ); - if (!alreadyExists) { - permissions.push(billingPerm); - logOnce('✅ Added com.android.vending.BILLING to AndroidManifest.xml'); - } else { - logOnce( - 'ℹ️ com.android.vending.BILLING already exists in AndroidManifest.xml', + if (props?.isFireOsEnabled) { + const nextPermissions = permissions.filter( + (p) => p.$['android:name'] !== 'com.android.vending.BILLING', ); - } - - // Add Meta Horizon App ID if provided - if (props?.horizonAppId) { - if ( - !manifest.manifest.application || - manifest.manifest.application.length === 0 - ) { - manifest.manifest.application = [ - {$: {'android:name': '.MainApplication'}}, - ]; - } - - const application = manifest.manifest.application![0]; - if (!application['meta-data']) { - application['meta-data'] = []; - } - - const metaData = application['meta-data']; - - // Use the correct meta-data name for Horizon Platform SDK - const horizonMetaDataName = 'com.meta.horizon.platform.ovr.OCULUS_APP_ID'; - const horizonAppIdMeta = { - $: { - 'android:name': horizonMetaDataName, - 'android:value': props.horizonAppId, - }, - }; - - const existingIndex = metaData.findIndex( - (m) => m.$['android:name'] === horizonMetaDataName, - ); - - if (existingIndex !== -1) { - metaData[existingIndex] = horizonAppIdMeta; + if (nextPermissions.length !== permissions.length) { + manifest.manifest['uses-permission'] = nextPermissions; logOnce( - `✅ Updated ${horizonMetaDataName} to ${props.horizonAppId} in AndroidManifest.xml`, + '🧹 Removed com.android.vending.BILLING from AndroidManifest.xml', ); + } + } else { + const alreadyExists = permissions.some( + (p) => p.$['android:name'] === 'com.android.vending.BILLING', + ); + if (!alreadyExists) { + permissions.push(billingPerm); + logOnce('✅ Added com.android.vending.BILLING to AndroidManifest.xml'); } else { - metaData.push(horizonAppIdMeta); logOnce( - `✅ Added ${horizonMetaDataName}: ${props.horizonAppId} to AndroidManifest.xml`, + 'ℹ️ com.android.vending.BILLING already exists in AndroidManifest.xml', ); } } + const horizonAppIdSync = syncHorizonAppIdMetaData( + manifest, + props?.isHorizonEnabled, + props?.horizonAppId, + ); + if (horizonAppIdSync === 'removed') { + logOnce( + `🧹 Removed ${HORIZON_APP_ID_META_DATA_NAME} from AndroidManifest.xml`, + ); + } else if (horizonAppIdSync === 'updated') { + logOnce( + `✅ Updated ${HORIZON_APP_ID_META_DATA_NAME} to ${props?.horizonAppId} in AndroidManifest.xml`, + ); + } else if (horizonAppIdSync === 'added') { + logOnce( + `✅ Added ${HORIZON_APP_ID_META_DATA_NAME}: ${props?.horizonAppId} to AndroidManifest.xml`, + ); + } + return config; }); @@ -537,6 +628,18 @@ export interface ExpoIapPluginOptions { }; /** Enable local development mode */ enableLocalDev?: boolean; + /** + * Horizon OS app ID for Quest devices. + * @deprecated Use android.horizonAppId instead. + * @platform android + */ + horizonAppId?: string; + /** + * iOS Alternative Billing configuration. + * @deprecated Use ios.alternativeBilling instead. + * @platform ios + */ + iosAlternativeBilling?: IOSAlternativeBillingConfig; /** * Optional modules configuration */ @@ -551,6 +654,16 @@ export interface ExpoIapPluginOptions { * @platform android */ horizon?: boolean; + /** + * Fire OS module for Amazon-distributed Android builds + * @platform android + */ + fireOS?: boolean; + /** + * Vega OS runtime target. This is not an Android flavor and cannot be + * combined with fireOS or horizon. + */ + vega?: boolean; }; /** * iOS-specific configuration @@ -626,6 +739,14 @@ const withIap: ConfigPlugin = ( config, options, ) => { + const isFireOsEnabled = options?.modules?.fireOS ?? false; + const isVegaEnabled = options?.modules?.vega ?? false; + if (isVegaEnabled && (isFireOsEnabled || options?.modules?.horizon)) { + throw new Error( + 'expo-iap: modules.vega cannot be combined with Fire OS or Horizon Android flavors. Vega OS is selected by the kepler runtime, not Gradle.', + ); + } + try { // Add iapkitApiKey to extra if provided if (options?.iapkitApiKey) { @@ -636,15 +757,19 @@ const withIap: ConfigPlugin = ( logOnce('🔑 [expo-iap] Added iapkitApiKey to config.extra'); } - // Read Horizon configuration from modules - const isHorizonEnabled = options?.modules?.horizon ?? false; + // Read Android store flavor configuration from modules. + const isHorizonEnabled = isFireOsEnabled + ? false + : (options?.modules?.horizon ?? false); const isOnsideEnabled = options?.modules?.onside ?? false; - const horizonAppId = options?.android?.horizonAppId; - const iosAlternativeBilling = options?.ios?.alternativeBilling; + const horizonAppId = + options?.android?.horizonAppId ?? options?.horizonAppId; + const iosAlternativeBilling = + options?.ios?.alternativeBilling ?? options?.iosAlternativeBilling; logOnce( - `🔍 [expo-iap] Config values: horizonAppId=${horizonAppId}, isHorizonEnabled=${isHorizonEnabled}, isOnsideEnabled=${isOnsideEnabled}`, + `🔍 [expo-iap] Config values: horizonAppId=${horizonAppId}, isHorizonEnabled=${isHorizonEnabled}, isFireOsEnabled=${isFireOsEnabled}, isVegaEnabled=${isVegaEnabled}, isOnsideEnabled=${isOnsideEnabled}`, ); const {includeExpoIap, includeOnside} = resolveModuleSelection( @@ -676,6 +801,7 @@ const withIap: ConfigPlugin = ( addDeps: !isLocalDev, horizonAppId, isHorizonEnabled, + isFireOsEnabled, }); // iOS: choose one path to avoid overlap @@ -706,7 +832,8 @@ const withIap: ConfigPlugin = ( localPath: resolved, iosAlternativeBilling, horizonAppId, - isHorizonEnabled, // Resolved from modules.horizon (line 467) + isHorizonEnabled, + isFireOsEnabled, }); } } else { diff --git a/libraries/expo-iap/plugin/src/withLocalOpenIAP.ts b/libraries/expo-iap/plugin/src/withLocalOpenIAP.ts index e59b2e9d..567f5c6c 100644 --- a/libraries/expo-iap/plugin/src/withLocalOpenIAP.ts +++ b/libraries/expo-iap/plugin/src/withLocalOpenIAP.ts @@ -15,7 +15,13 @@ import { * Plugin to add local OpenIAP pod dependency for development * This is only for local development with openiap-apple library */ -type LocalPathOption = string | {ios?: string; android?: string}; +export type LocalPathOption = string | {ios?: string; android?: string}; + +export const getAndroidLocalPathInput = ( + raw?: LocalPathOption, +): string | undefined => { + return typeof raw === 'string' ? raw : raw?.android; +}; interface AndroidGradlePluginVersions { kotlin: string; @@ -93,6 +99,8 @@ const withLocalOpenIAP: ConfigPlugin< horizonAppId?: string; /** Resolved from modules.horizon by withIAP */ isHorizonEnabled?: boolean; + /** Resolved from modules.fireOS by withIAP */ + isFireOsEnabled?: boolean; } | void > = (config, props) => { // Import and apply iOS alternative billing configuration if provided @@ -178,7 +186,7 @@ const withLocalOpenIAP: ConfigPlugin< config = withSettingsGradle(config, (config) => { const raw = props?.localPath; const projectRoot = (config.modRequest as any).projectRoot as string; - const androidInput = typeof raw === 'string' ? undefined : raw?.android; + const androidInput = getAndroidLocalPathInput(raw); const androidModulePath = resolveAndroidModulePath(androidInput) || resolveAndroidModulePath(path.resolve(projectRoot, 'openiap-google')) || @@ -294,7 +302,7 @@ const withLocalOpenIAP: ConfigPlugin< config = withAppBuildGradle(config, (config) => { const projectRoot = (config.modRequest as any).projectRoot as string; const raw = props?.localPath; - const androidInput = typeof raw === 'string' ? undefined : raw?.android; + const androidInput = getAndroidLocalPathInput(raw); const androidModulePath = resolveAndroidModulePath(androidInput) || resolveAndroidModulePath(path.resolve(projectRoot, 'openiap-google')) || @@ -306,15 +314,19 @@ const withLocalOpenIAP: ConfigPlugin< const gradle = config.modResults; const dependencyLine = ` implementation project(':openiap-google')`; - const flavor = props?.isHorizonEnabled ? 'horizon' : 'play'; + const flavor = props?.isFireOsEnabled + ? 'amazon' + : props?.isHorizonEnabled + ? 'horizon' + : 'play'; const strategyLine = ` missingDimensionStrategy "platform", "${flavor}"`; let contents = gradle.contents; - // Remove Maven deps (both openiap-google and openiap-google-horizon) + // Remove Maven deps for all openiap-google flavors // to avoid duplicate classes with local module const mavenPattern = - /^\s*(?:implementation|api)\s*\(?\s*["']io\.github\.hyochan\.openiap:openiap-google(?:-horizon)?:[^"']+["']\s*\)?\s*$/gm; + /^\s*(?:implementation|api)\s*\(?\s*["']io\.github\.hyochan\.openiap:openiap-google(?:-(?:horizon|amazon))?:[^"']+["']\s*\)?\s*$/gm; if (mavenPattern.test(contents)) { contents = contents.replace(mavenPattern, '\n'); logOnce( @@ -325,7 +337,7 @@ const withLocalOpenIAP: ConfigPlugin< // Add missingDimensionStrategy (required for flavored module) // Remove any existing platform strategies first to avoid duplicates const strategyPattern = - /^\s*missingDimensionStrategy\s*\(?\s*["']platform["']\s*,\s*["'](play|horizon)["']\s*\)?\s*$/gm; + /^\s*missingDimensionStrategy\s*\(?\s*["']platform["']\s*,\s*["'](play|horizon|amazon)["']\s*\)?\s*$/gm; if (strategyPattern.test(contents)) { contents = contents.replace(strategyPattern, ''); logOnce('🧹 Removed existing missingDimensionStrategy for platform'); @@ -358,7 +370,7 @@ const withLocalOpenIAP: ConfigPlugin< return config; }); - // 3) Set horizonEnabled in gradle.properties + // 3) Set store flags in gradle.properties config = withDangerousMod(config, [ 'android', async (config) => { @@ -371,16 +383,21 @@ const withLocalOpenIAP: ConfigPlugin< if (fs.existsSync(gradlePropertiesPath)) { let contents = fs.readFileSync(gradlePropertiesPath, 'utf8'); const isHorizon = props?.isHorizonEnabled ?? false; + const isFireOS = props?.isFireOsEnabled ?? false; - // Update horizonEnabled property contents = contents.replace(/^horizonEnabled=.*$/gm, ''); + contents = contents.replace(/^fireOsEnabled=.*$/gm, ''); if (!contents.endsWith('\n')) contents += '\n'; contents += `horizonEnabled=${isHorizon}\n`; + contents += `fireOsEnabled=${isFireOS}\n`; fs.writeFileSync(gradlePropertiesPath, contents); logOnce( `🛠️ expo-iap: Set horizonEnabled=${isHorizon} in gradle.properties`, ); + logOnce( + `🛠️ expo-iap: Set fireOsEnabled=${isFireOS} in gradle.properties`, + ); } return config; diff --git a/libraries/expo-iap/src/ExpoIapModule.ts b/libraries/expo-iap/src/ExpoIapModule.ts index 35428b63..7b97ad71 100644 --- a/libraries/expo-iap/src/ExpoIapModule.ts +++ b/libraries/expo-iap/src/ExpoIapModule.ts @@ -1,59 +1,75 @@ import {requireNativeModule, UnavailabilityError} from 'expo-modules-core'; import {installedFromOnside} from './onside'; +import {getVegaIapModule, isVegaOS} from './vega'; -type NativeIapModuleName = 'ExpoIapOnside' | 'ExpoIap'; +type NativeIapModuleName = 'ExpoIapVega' | 'ExpoIapOnside' | 'ExpoIap'; const ONSIDE_MARKETPLACE_ID = 'com.onside.marketplace-app'; let cached: {module: any; name: NativeIapModuleName} | null = null; let expoIapFallback: any | null | undefined; let onsideModuleUnavailable = false; -function isOnsideInstallation(): boolean { - if (installedFromOnside === true) { - return true; - } +function getResolved(): {module: any; name: NativeIapModuleName} { + function shouldUseOnsideModule(): boolean { + if (installedFromOnside === true) { + return true; + } - if (typeof installedFromOnside !== 'string') { - return false; - } + if (typeof installedFromOnside !== 'string') { + return false; + } - const normalized = installedFromOnside.trim().toLowerCase(); - return normalized === 'true' || normalized === ONSIDE_MARKETPLACE_ID; -} + const normalized = installedFromOnside.trim().toLowerCase(); + return normalized === 'true' || normalized === ONSIDE_MARKETPLACE_ID; + } -function shouldUseOnsideModule(): boolean { - return isOnsideInstallation() && !onsideModuleUnavailable; -} + function getExpectedModuleName(): NativeIapModuleName { + if (isVegaOS()) { + return 'ExpoIapVega'; + } -function getResolved(): {module: any; name: NativeIapModuleName} { - const expectedName: NativeIapModuleName = shouldUseOnsideModule() - ? 'ExpoIapOnside' - : 'ExpoIap'; - if (!cached || cached.name !== expectedName) { - cached = resolveNativeModule(); + return shouldUseOnsideModule() && !onsideModuleUnavailable + ? 'ExpoIapOnside' + : 'ExpoIap'; } - return cached; -} -function resolveNativeModule(): { - module: any; - name: NativeIapModuleName; -} { - if (isOnsideInstallation()) { - try { - return { - module: requireNativeModule('ExpoIapOnside'), - name: 'ExpoIapOnside', - }; - } catch (error) { - if (!isMissingModuleError(error, 'ExpoIapOnside')) { - throw error; + function resolveNativeModule(): { + module: any; + name: NativeIapModuleName; + } { + if (isVegaOS()) { + const vegaModule = getVegaIapModule(); + if (!vegaModule) { + throw new UnavailabilityError( + 'expo-iap', + 'Amazon Vega IAP module is unavailable. Add @amazon-devices/keplerscript-appstore-iap-lib and build with the React Native Vega kepler platform.', + ); + } + return {module: vegaModule, name: 'ExpoIapVega'}; + } + + if (shouldUseOnsideModule()) { + try { + return { + module: requireNativeModule('ExpoIapOnside'), + name: 'ExpoIapOnside', + }; + } catch (error) { + if (!isMissingModuleError(error, 'ExpoIapOnside')) { + throw error; + } + onsideModuleUnavailable = true; } - onsideModuleUnavailable = true; } + + return {module: requireNativeModule('ExpoIap'), name: 'ExpoIap'}; } - return {module: requireNativeModule('ExpoIap'), name: 'ExpoIap'}; + const expectedName = getExpectedModuleName(); + if (!cached || cached.name !== expectedName) { + cached = resolveNativeModule(); + } + return cached; } function isMissingModuleError(error: unknown, moduleName: string): boolean { @@ -88,31 +104,34 @@ export function getNativeModule() { return getResolved().module; } -function getExpoIapFallbackModule(): any | null { - if (expoIapFallback !== undefined) { - return expoIapFallback; - } +export default new Proxy({} as any, { + get(target, prop) { + function getExpoIapFallbackModule(): any | null { + if (expoIapFallback !== undefined) { + return expoIapFallback; + } - try { - expoIapFallback = requireNativeModule('ExpoIap'); - } catch (error) { - if (isMissingModuleError(error, 'ExpoIap')) { - expoIapFallback = null; - } else { - throw error; - } - } + try { + expoIapFallback = requireNativeModule('ExpoIap'); + } catch (error) { + if (isMissingModuleError(error, 'ExpoIap')) { + expoIapFallback = null; + } else { + throw error; + } + } - return expoIapFallback; -} + return expoIapFallback; + } -export default new Proxy({} as any, { - get(target, prop) { if (typeof prop === 'symbol') return Reflect.get(target, prop); const resolved = getResolved(); if (prop === 'USING_ONSIDE_SDK') { return resolved.name === 'ExpoIapOnside'; } + if (prop === 'USING_VEGA_SDK') { + return resolved.name === 'ExpoIapVega'; + } const value = resolved.module[prop]; if (value !== undefined || resolved.name !== 'ExpoIapOnside') { diff --git a/libraries/expo-iap/src/__tests__/native-log-redaction.test.js b/libraries/expo-iap/src/__tests__/native-log-redaction.test.js new file mode 100644 index 00000000..f0a9b844 --- /dev/null +++ b/libraries/expo-iap/src/__tests__/native-log-redaction.test.js @@ -0,0 +1,62 @@ +/* eslint-env jest, node */ + +const {readFileSync} = require('fs'); +const {resolve} = require('path'); + +const rootDir = resolve(__dirname, '../..'); + +function readRepoFile(path) { + return readFileSync(resolve(rootDir, path), 'utf8'); +} + +describe('native log redaction', () => { + it('keeps Expo native log sanitizers covering verification secrets', () => { + const androidLog = readRepoFile( + 'android/src/main/java/expo/modules/iap/ExpoIapLog.kt', + ); + const iosLog = readRepoFile('ios/ExpoIapLog.swift'); + const sensitiveFragments = [ + 'token', + 'apikey', + 'secret', + 'jws', + 'receiptid', + 'userid', + ]; + + for (const fragment of sensitiveFragments) { + expect(androidLog).toContain(`"${fragment}"`); + expect(iosLog).toContain(`"${fragment}"`); + } + expect(androidLog).toContain('isSensitiveKey'); + expect(iosLog).toContain('isSensitiveKey'); + expect(androidLog).toContain('filter { it.isLetterOrDigit() }'); + expect(iosLog).toContain('.filter { $0.isLetter || $0.isNumber }'); + }); + + it('does not log raw verifyPurchaseWithProvider params on Expo native bridges', () => { + const androidModule = readRepoFile( + 'android/src/main/java/expo/modules/iap/ExpoIapModule.kt', + ); + const iosModule = readRepoFile('ios/ExpoIapModule.swift'); + + expect(androidModule).not.toContain( + 'ExpoIapLog.payload("verifyPurchaseWithProvider", params)', + ); + expect(iosModule).not.toContain( + 'ExpoIapLog.payload("verifyPurchaseWithProvider", payload: params)', + ); + expect(androidModule).toContain('"hasIapkit" to (params["iapkit"] != null)'); + expect(iosModule).toContain('"hasIapkit": params["iapkit"] != nil'); + }); + + it('does not log raw IAPKit request bodies in the Apple core package', () => { + const appleModule = readFileSync( + resolve(rootDir, '../../packages/apple/Sources/OpenIapModule.swift'), + 'utf8', + ); + + expect(appleModule).not.toContain('IAPKit request body:'); + expect(appleModule).toContain('IAPKit request body bytes='); + }); +}); diff --git a/libraries/expo-iap/src/__tests__/vega-adapter.test.ts b/libraries/expo-iap/src/__tests__/vega-adapter.test.ts new file mode 100644 index 00000000..5858fb89 --- /dev/null +++ b/libraries/expo-iap/src/__tests__/vega-adapter.test.ts @@ -0,0 +1,509 @@ +import { + createExpoIapVegaModule, + type VegaPurchasingService, +} from '../vega-adapter'; +import {ErrorCode} from '../types'; + +const createService = (): jest.Mocked => + ({ + getUserData: jest.fn(async () => ({ + responseCode: 1, + userData: { + countryCode: 'US', + marketplace: 'US', + userId: 'amazon-user', + }, + })), + getProductData: jest.fn(async () => ({ + responseCode: 1, + productData: { + coins_100: { + sku: 'coins_100', + title: '100 Coins', + description: 'Coin pack', + productType: 1, + price: { + priceCurrencyCode: 'USD', + priceStr: '$0.99', + valueInMicros: 990000, + }, + }, + premium_monthly: { + sku: 'premium_monthly', + title: 'Premium Monthly', + description: 'Monthly plan', + productType: 3, + subscriptionPeriod: 'P1M', + price: { + priceCurrencyCode: 'USD', + priceStr: '$4.99', + valueInMicros: 4990000, + }, + }, + }, + })), + purchase: jest.fn(async () => ({ + responseCode: 0, + receipt: { + receiptId: 'receipt-1', + sku: 'coins_100', + productType: 1, + purchaseDate: new Date('2026-05-11T00:00:00.000Z'), + }, + })), + getPurchaseUpdates: jest.fn(async () => ({ + responseCode: 1, + receiptList: [ + { + receiptId: 'sub-receipt', + sku: 'premium_monthly', + productType: 3, + purchaseDate: new Date('2026-05-10T00:00:00.000Z'), + }, + ], + })), + notifyFulfillment: jest.fn(async () => ({ + responseCode: 1, + })), + }) as unknown as jest.Mocked; + +describe('Amazon Vega Expo adapter', () => { + it('maps Vega product data to OpenIAP Android products', async () => { + const service = createService(); + const module = createExpoIapVegaModule(service); + + const products = await module.fetchProducts('all', [ + 'coins_100', + 'premium_monthly', + ]); + + expect(service.getProductData).toHaveBeenCalledWith({ + skus: ['coins_100', 'premium_monthly'], + }); + expect(products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 'coins_100', + type: 'in-app', + platform: 'android', + }), + expect.objectContaining({ + id: 'premium_monthly', + type: 'subs', + platform: 'android', + subscriptionOfferDetailsAndroid: expect.any(Array), + }), + ]), + ); + }); + + it('accepts Amazon Vega string success response codes', async () => { + const service = createService(); + service.getProductData.mockResolvedValueOnce({ + responseCode: 'SUCCESSFUL', + productData: new Map([ + [ + 'coins_100', + { + sku: 'coins_100', + title: '100 Coins', + description: 'Coin pack', + productType: 1, + price: { + priceCurrencyCode: 'USD', + priceStr: '$0.99', + valueInMicros: '990000', + }, + }, + ], + ]), + }); + const module = createExpoIapVegaModule(service); + + await expect(module.fetchProducts('all', ['coins_100'])).resolves.toEqual([ + expect.objectContaining({ + id: 'coins_100', + price: 0.99, + }), + ]); + }); + + it('emits purchase updates and acknowledges receipts', async () => { + const service = createService(); + const module = createExpoIapVegaModule(service); + const listener = jest.fn(); + + module.addListener('purchase-updated', listener); + const result = await module.requestPurchase({ + skuArr: ['coins_100'], + type: 'in-app', + }); + + expect(result).toEqual([ + expect.objectContaining({ + productId: 'coins_100', + purchaseToken: 'receipt-1', + store: 'amazon', + }), + ]); + expect(listener).toHaveBeenCalledWith( + expect.objectContaining({ + productId: 'coins_100', + purchaseToken: 'receipt-1', + }), + ); + + await module.acknowledgePurchaseAndroid('receipt-1'); + + expect(service.notifyFulfillment).toHaveBeenCalledWith({ + fulfillmentResult: 1, + receiptId: 'receipt-1', + }); + }); + + it('maps Amazon invalid SKU purchase failures to OpenIAP errors', async () => { + const service = createService(); + service.purchase.mockResolvedValueOnce({ + responseCode: 2, + receipt: null, + }); + const module = createExpoIapVegaModule(service); + const errorListener = jest.fn(); + module.addListener('purchase-error', errorListener); + + await expect( + module.requestPurchase({ + skuArr: ['missing_sku'], + type: 'in-app', + }), + ).rejects.toMatchObject({ + code: ErrorCode.SkuNotFound, + productId: 'missing_sku', + }); + expect(errorListener).toHaveBeenCalledWith( + expect.objectContaining({ + code: ErrorCode.SkuNotFound, + productId: 'missing_sku', + responseCode: 2, + }), + ); + }); + + it('returns active subscriptions from purchase updates', async () => { + const service = createService(); + const module = createExpoIapVegaModule(service); + + const subscriptions = await module.getActiveSubscriptions([ + 'premium_monthly', + ]); + + expect(service.getPurchaseUpdates).toHaveBeenCalledWith({reset: true}); + expect(subscriptions).toEqual([ + expect.objectContaining({ + productId: 'premium_monthly', + isActive: true, + purchaseToken: 'sub-receipt', + }), + ]); + }); + + it('uses cached product types when purchase updates omit productType', async () => { + const service = createService(); + service.getPurchaseUpdates.mockResolvedValue({ + responseCode: 1, + receiptList: [ + { + receiptId: 'sub-receipt', + sku: 'premium_monthly', + purchaseDate: new Date('2026-05-10T00:00:00.000Z'), + }, + ], + }); + const module = createExpoIapVegaModule(service); + + await module.fetchProducts('subs', ['premium_monthly']); + + await expect( + module.getActiveSubscriptions(['premium_monthly']), + ).resolves.toEqual([ + expect.objectContaining({ + productId: 'premium_monthly', + isActive: true, + purchaseToken: 'sub-receipt', + }), + ]); + }); + + it('hydrates product types when purchase updates omit productType before fetchProducts', async () => { + const service = createService(); + service.getPurchaseUpdates.mockResolvedValue({ + responseCode: 1, + receiptList: [ + { + receiptId: 'sub-receipt', + sku: 'premium_monthly', + purchaseDate: new Date('2026-05-10T00:00:00.000Z'), + }, + ], + }); + const module = createExpoIapVegaModule(service); + + await expect( + module.getActiveSubscriptions(['premium_monthly']), + ).resolves.toEqual([ + expect.objectContaining({ + productId: 'premium_monthly', + isActive: true, + purchaseToken: 'sub-receipt', + }), + ]); + expect(service.getProductData).toHaveBeenCalledWith({ + skus: ['premium_monthly'], + }); + }); + + it('uses subscription request context when purchase receipts omit productType', async () => { + const service = createService(); + service.purchase.mockResolvedValueOnce({ + responseCode: 0, + receipt: { + receiptId: 'sub-purchase', + sku: 'premium_monthly', + }, + }); + const module = createExpoIapVegaModule(service); + + await expect( + module.requestPurchase({ + skuArr: ['premium_monthly'], + type: 'subs', + }), + ).resolves.toEqual([ + expect.objectContaining({ + productId: 'premium_monthly', + isAutoRenewing: true, + autoRenewingAndroid: true, + }), + ]); + }); + + it('loads all paginated Amazon purchase updates', async () => { + const service = createService(); + service.getPurchaseUpdates + .mockResolvedValueOnce({ + responseCode: 1, + hasMore: true, + receiptList: [ + { + receiptId: 'receipt-page-1', + sku: 'coins_100', + productType: 1, + }, + ], + }) + .mockResolvedValueOnce({ + responseCode: 1, + hasMore: false, + receiptList: [ + { + receiptId: 'receipt-page-2', + sku: 'premium_monthly', + productType: 3, + }, + ], + }); + const module = createExpoIapVegaModule(service); + + const purchases = await module.getAvailableItems(); + + expect(service.getPurchaseUpdates).toHaveBeenNthCalledWith(1, { + reset: true, + }); + expect(service.getPurchaseUpdates).toHaveBeenNthCalledWith(2, { + reset: false, + }); + expect(purchases.map((purchase) => purchase.id)).toEqual([ + 'receipt-page-1', + 'receipt-page-2', + ]); + }); + + it('excludes suspended purchases unless requested', async () => { + const service = createService(); + service.getPurchaseUpdates.mockResolvedValue({ + responseCode: 1, + receiptList: [ + { + receiptId: 'deferred-sub', + sku: 'premium_monthly', + productType: 3, + isDeferred: true, + }, + ], + }); + const module = createExpoIapVegaModule(service); + + await expect(module.getAvailableItems()).resolves.toEqual([]); + + await expect( + module.getAvailableItems({includeSuspendedAndroid: true}), + ).resolves.toEqual([ + expect.objectContaining({ + id: 'deferred-sub', + isAutoRenewing: false, + isSuspendedAndroid: true, + purchaseState: 'pending', + }), + ]); + }); + + it('verifies Vega receipts through IAPKit Amazon payload', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + Response.json({ + isValid: true, + state: 'ENTITLED', + store: 'amazon', + }), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createExpoIapVegaModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + apiKey: 'kit-key', + amazon: { + receiptId: 'receipt-vega-1', + sandbox: true, + }, + }, + }), + ).resolves.toEqual({ + provider: 'iapkit', + iapkit: { + isValid: true, + state: 'entitled', + store: 'amazon', + }, + }); + + expect(service.getUserData).toHaveBeenCalledWith({}); + expect(fetchMock).toHaveBeenCalledWith( + 'https://kit.openiap.dev/v1/purchase/verify', + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ + Authorization: 'Bearer kit-key', + 'Content-Type': 'application/json', + }), + }), + ); + expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({ + store: 'amazon', + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + sandbox: true, + }); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('wraps non-JSON IAPKit failures as receipt errors', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + new Response('bad gateway', {status: 502}), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createExpoIapVegaModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toMatchObject({ + code: ErrorCode.ReceiptFailed, + message: 'HTTP 502', + }); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('rejects empty successful IAPKit responses as receipt errors', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + new Response('', {status: 200}), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createExpoIapVegaModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toMatchObject({ + code: ErrorCode.ReceiptFailed, + message: 'IAPKit returned non-JSON response (HTTP 200).', + }); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('wraps IAPKit network failures as network errors', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn(async () => { + throw new TypeError('network offline'); + }) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createExpoIapVegaModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toMatchObject({ + code: ErrorCode.NetworkError, + message: 'network offline', + }); + } finally { + globalThis.fetch = originalFetch; + } + }); +}); diff --git a/libraries/expo-iap/src/amazon-devices-kepler.d.ts b/libraries/expo-iap/src/amazon-devices-kepler.d.ts new file mode 100644 index 00000000..b443183e --- /dev/null +++ b/libraries/expo-iap/src/amazon-devices-kepler.d.ts @@ -0,0 +1,12 @@ +declare module '@amazon-devices/keplerscript-appstore-iap-lib' { + export const PurchasingService: { + getProductData(request: {skus: string[]}): Promise; + getPurchaseUpdates(request: {reset: boolean}): Promise; + getUserData(request: Record): Promise; + notifyFulfillment(request: { + fulfillmentResult: number; + receiptId: string; + }): Promise; + purchase(request: {sku: string}): Promise; + }; +} diff --git a/libraries/expo-iap/src/index.ts b/libraries/expo-iap/src/index.ts index 32a61f19..6852d608 100644 --- a/libraries/expo-iap/src/index.ts +++ b/libraries/expo-iap/src/index.ts @@ -3,6 +3,7 @@ import {Platform} from 'react-native'; // Internal modules import ExpoIapModule, {getNativeModule} from './ExpoIapModule'; +import {isVegaOS} from './vega'; import { isProductIOS, validateReceiptIOS, @@ -48,6 +49,7 @@ export * from './types'; export * from './modules/android'; export * from './modules/ios'; export * from './onside'; +export * from './vega'; // Get the native constant value export enum OpenIapEvent { @@ -266,6 +268,10 @@ const normalizePurchasePlatform = (purchase: Purchase): Purchase => { const normalizePurchaseArray = (purchases: Purchase[]): Purchase[] => purchases.map((purchase) => normalizePurchasePlatform(purchase)); +const isAndroidStoreRuntime = (): boolean => { + return Platform.OS === 'android' || isVegaOS(); +}; + export const purchaseUpdatedListener = ( listener: (event: Purchase) => void, options?: PurchaseUpdatedListenerOptions | null, @@ -681,7 +687,7 @@ export const fetchProducts: QueryField<'fetchProducts'> = async (request) => { return castResult(filterIosItems(rawItems)); } - if (Platform.OS === 'android') { + if (isAndroidStoreRuntime()) { const rawItems = await ExpoIapModule.fetchProducts(native, skus); return castResult(filterAndroidItems(rawItems)); } @@ -718,6 +724,11 @@ export const getAvailablePurchases: QueryField< includeSuspendedAndroid: options?.includeSuspendedAndroid ?? false, }; + if (isVegaOS()) { + const purchases = await ExpoIapModule.getAvailableItems(normalizedOptions); + return normalizePurchaseArray(purchases as Purchase[]); + } + let purchases: Purchase[]; if (Platform.OS === 'ios') { @@ -814,7 +825,7 @@ export const hasActiveSubscriptions: QueryField< * @see {@link https://openiap.dev/docs/apis/get-storefront} */ export const getStorefront: QueryField<'getStorefront'> = async () => { - if (Platform.OS !== 'ios' && Platform.OS !== 'android') { + if (Platform.OS !== 'ios' && !isAndroidStoreRuntime()) { return ''; } return ExpoIapModule.getStorefront(); @@ -930,7 +941,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( return canonical === 'subs' ? [] : null; } - if (Platform.OS === 'android') { + if (isAndroidStoreRuntime()) { if (isInAppPurchase) { const normalizedRequest = normalizeRequestProps( request as RequestPurchasePropsByPlatforms, @@ -1070,7 +1081,7 @@ export const finishTransaction: MutationField<'finishTransaction'> = async ({ return; } - if (Platform.OS === 'android') { + if (isAndroidStoreRuntime()) { const token = purchase.purchaseToken ?? undefined; if (!token) { diff --git a/libraries/expo-iap/src/types.ts b/libraries/expo-iap/src/types.ts index 518f5ffd..2e3ddc63 100644 --- a/libraries/expo-iap/src/types.ts +++ b/libraries/expo-iap/src/types.ts @@ -511,7 +511,7 @@ export type IapEvent = 'purchase-updated' | 'purchase-error' | 'promoted-product export type IapPlatform = 'ios' | 'android'; -export type IapStore = 'unknown' | 'apple' | 'google' | 'horizon'; +export type IapStore = 'unknown' | 'apple' | 'google' | 'horizon' | 'amazon'; /** Unified purchase states from IAPKit verification response. */ export type IapkitPurchaseState = 'entitled' | 'pending-acknowledgment' | 'pending' | 'canceled' | 'expired' | 'ready-to-consume' | 'consumed' | 'unknown' | 'inauthentic'; @@ -1585,7 +1585,8 @@ export type RequestPurchaseProps = * * Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. * - apple: Always targets App Store - * - google: Targets Play Store by default, or Horizon when built with horizon flavor + * - google: Targets Play Store by default, Horizon when built with horizon flavor, + * or Fire OS when built with amazon flavor * (determined at build time, not runtime) */ export interface RequestPurchasePropsByPlatforms { @@ -1679,7 +1680,8 @@ export interface RequestSubscriptionIosProps { * * Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. * - apple: Always targets App Store - * - google: Targets Play Store by default, or Horizon when built with horizon flavor + * - google: Targets Play Store by default, Horizon when built with horizon flavor, + * or Fire OS when built with amazon flavor * (determined at build time, not runtime) */ export interface RequestSubscriptionPropsByPlatforms { @@ -1693,6 +1695,15 @@ export interface RequestSubscriptionPropsByPlatforms { ios?: (RequestSubscriptionIosProps | null); } +export interface RequestVerifyPurchaseWithIapkitAmazonProps { + /** Amazon Appstore receipt id returned by PurchaseResponse.getReceipt().getReceiptId(). */ + receiptId: string; + /** Use Amazon RVS Cloud Sandbox for App Tester receipts. */ + sandbox?: (boolean | null); + /** Amazon Appstore user id returned by PurchaseResponse.getUserData().getUserId(). */ + userId?: (string | null); +} + export interface RequestVerifyPurchaseWithIapkitAppleProps { /** The JWS token returned with the purchase response. */ jws: string; @@ -1708,8 +1719,11 @@ export interface RequestVerifyPurchaseWithIapkitGoogleProps { * * - apple: Verifies via App Store (JWS token) * - google: Verifies via Play Store (purchase token) + * - amazon: Verifies via Amazon Appstore RVS (userId + receiptId) */ export interface RequestVerifyPurchaseWithIapkitProps { + /** Amazon Appstore verification parameters. */ + amazon?: (RequestVerifyPurchaseWithIapkitAmazonProps | null); /** API key used for the Authorization header (Bearer {apiKey}). */ apiKey?: (string | null); /** Apple App Store verification parameters. */ diff --git a/libraries/expo-iap/src/vega-adapter.ts b/libraries/expo-iap/src/vega-adapter.ts new file mode 100644 index 00000000..7c8e4382 --- /dev/null +++ b/libraries/expo-iap/src/vega-adapter.ts @@ -0,0 +1,847 @@ +import {ErrorCode} from './types'; +import type { + ActiveSubscription, + IapkitPurchaseState, + Product, + ProductSubscription, + Purchase, + PurchaseOptions, + VerifyPurchaseWithProviderProps, + VerifyPurchaseWithProviderResult, +} from './types'; + +type ResponseOperation = + | 'product-data' + | 'purchase' + | 'purchase-updates' + | 'user-data' + | 'notify-fulfillment'; + +type VegaListener = (payload: any) => void; + +interface VegaPurchaseErrorPayload { + code: ErrorCode; + debugMessage?: string; + message: string; + productId?: string; + responseCode?: number; +} + +interface VegaPrice { + priceCurrencyCode?: string | null; + priceStr?: string | null; + valueInMicros?: bigint | number | string | null; +} + +interface VegaProduct { + description?: string | null; + freeTrialPeriod?: string | null; + price?: VegaPrice | null; + productType?: unknown; + sku?: string | null; + subscriptionPeriod?: string | null; + title?: string | null; +} + +interface VegaReceipt { + cancelDate?: Date | number | string | null; + deferredDate?: Date | number | string | null; + isCancelled?: boolean | null; + isDeferred?: boolean | null; + productType?: unknown; + purchaseDate?: Date | number | string | null; + receiptId?: string | null; + sku?: string | null; + termSku?: string | null; +} + +interface VegaUserData { + countryCode?: string | null; + marketplace?: string | null; + userId?: string | null; +} + +interface VegaResponse { + responseCode?: unknown; +} + +interface VegaProductDataResponse extends VegaResponse { + productData?: Map | Record | null; +} + +interface VegaPurchaseResponse extends VegaResponse { + receipt?: VegaReceipt | null; + userData?: VegaUserData | null; +} + +interface VegaPurchaseUpdatesResponse extends VegaResponse { + hasMore?: boolean | null; + receiptList?: VegaReceipt[] | null; + userData?: VegaUserData | null; +} + +interface VegaUserDataResponse extends VegaResponse { + userData?: VegaUserData | null; +} + +export interface VegaPurchasingService { + getProductData(request: {skus: string[]}): Promise; + getPurchaseUpdates(request: { + reset: boolean; + }): Promise; + getUserData(request: Record): Promise; + notifyFulfillment(request: { + fulfillmentResult: number; + receiptId: string; + }): Promise; + purchase(request: {sku: string}): Promise; +} + +export interface ExpoIapVegaModule { + ERROR_CODES: Record; + acknowledgePurchaseAndroid(purchaseToken: string): Promise; + addListener(eventName: string, listener: VegaListener): {remove: () => void}; + consumePurchaseAndroid(purchaseToken: string): Promise; + endConnection(): Promise; + fetchProducts( + type: string, + skus: string[], + ): Promise<(Product | ProductSubscription)[]>; + getActiveSubscriptions( + subscriptionIds?: string[] | null, + ): Promise; + getAvailableItems(options?: PurchaseOptions): Promise; + getStorefront(): Promise; + hasActiveSubscriptions(subscriptionIds?: string[] | null): Promise; + initConnection(config?: unknown): Promise; + removeListener(eventName: string, listener: VegaListener): void; + requestPurchase(params: { + skuArr?: string[]; + type?: string; + }): Promise; + verifyPurchaseWithProvider( + options: VerifyPurchaseWithProviderProps, + ): Promise; +} + +const PRODUCT_TYPE_SUBSCRIPTION = 3; +const FULFILLMENT_RESULT_FULFILLED = 1; +const RESPONSE_SUCCESS = 1; +const PURCHASE_RESPONSE_SUCCESS = 0; +const IAPKIT_VERIFY_URL = 'https://kit.openiap.dev/v1/purchase/verify'; + +function createVegaError( + code: ErrorCode, + message: string, + responseCode?: unknown, + productId?: string, +): Error { + const error = new Error(message) as Error & { + code?: ErrorCode; + debugMessage?: string; + productId?: string; + responseCode?: number; + }; + error.code = code; + error.debugMessage = message; + error.productId = productId; + if (typeof responseCode === 'number') { + error.responseCode = responseCode; + } + return error; +} + +function toPurchaseErrorPayload( + error: unknown, + fallbackMessage: string, + productId?: string, +): VegaPurchaseErrorPayload { + if (error instanceof Error) { + const errorWithFields = error as Error & { + code?: ErrorCode; + debugMessage?: string; + productId?: string; + responseCode?: number; + }; + return { + code: errorWithFields.code ?? ErrorCode.PurchaseError, + debugMessage: errorWithFields.debugMessage, + message: error.message || fallbackMessage, + productId: errorWithFields.productId ?? productId, + responseCode: errorWithFields.responseCode, + }; + } + + return { + code: ErrorCode.PurchaseError, + message: fallbackMessage, + productId, + }; +} + +function responseCodeName(responseCode: unknown): string { + return typeof responseCode === 'string' ? responseCode.toUpperCase() : ''; +} + +function isSuccess( + operation: ResponseOperation, + responseCode: unknown, +): boolean { + if (typeof responseCode === 'number') { + return operation === 'purchase' + ? responseCode === PURCHASE_RESPONSE_SUCCESS + : responseCode === RESPONSE_SUCCESS; + } + + const name = responseCodeName(responseCode); + return name === 'SUCCESSFUL' || name === 'SUCCESS' || name === 'OK'; +} + +function mapErrorCode( + operation: ResponseOperation, + responseCode: unknown, +): ErrorCode { + const name = responseCodeName(responseCode); + if (name.includes('ALREADY_PURCHASED')) return ErrorCode.AlreadyOwned; + if (name.includes('INVALID_SKU')) return ErrorCode.SkuNotFound; + if (name.includes('NOT_SUPPORTED')) return ErrorCode.FeatureNotSupported; + if (name.includes('PENDING')) return ErrorCode.Pending; + if (operation === 'purchase' && name.includes('FAILED')) { + return ErrorCode.UserCancelled; + } + + if (typeof responseCode === 'number') { + if (operation === 'purchase') { + if (responseCode === 1) return ErrorCode.AlreadyOwned; + if (responseCode === 2) return ErrorCode.SkuNotFound; + if (responseCode === 3) return ErrorCode.FeatureNotSupported; + if (responseCode === 4) return ErrorCode.UserCancelled; + } + if (responseCode === 2 && operation !== 'purchase') { + return ErrorCode.FeatureNotSupported; + } + } + + if (operation === 'product-data') return ErrorCode.QueryProduct; + if (operation === 'user-data') return ErrorCode.InitConnection; + return ErrorCode.PurchaseError; +} + +function ensureSuccessful( + operation: ResponseOperation, + response: VegaResponse | null | undefined, + message: string, + productId?: string, +): void { + const responseCode = response?.responseCode; + if (isSuccess(operation, responseCode)) return; + throw createVegaError( + mapErrorCode(operation, responseCode), + `${message}. Amazon Vega responseCode=${String(responseCode ?? 'unknown')}`, + responseCode, + productId, + ); +} + +function toTimestamp(value: unknown): number { + if (value instanceof Date) return value.getTime(); + if (typeof value === 'number' && Number.isFinite(value)) return value; + if (typeof value === 'string') { + const timestamp = Date.parse(value); + return Number.isFinite(timestamp) ? timestamp : Date.now(); + } + return Date.now(); +} + +function toPriceAmountMicros(value: unknown): string { + if (typeof value === 'bigint') return value.toString(); + if (typeof value === 'number' && Number.isFinite(value)) { + return Math.trunc(value).toString(); + } + if (typeof value === 'string' && value.length > 0) return value; + return '0'; +} + +function microsToPrice(value: unknown): number | null { + const micros = + typeof value === 'bigint' ? Number(value) : Number(value ?? Number.NaN); + if (!Number.isFinite(micros)) return null; + return micros / 1_000_000; +} + +function isSubscription(productType: unknown): boolean { + if (typeof productType === 'number') + return productType === PRODUCT_TYPE_SUBSCRIPTION; + if (typeof productType === 'string') { + return productType.toUpperCase().includes('SUBSCRIPTION'); + } + return false; +} + +function productTypeToOpenIap(productType: unknown): 'in-app' | 'subs' { + return isSubscription(productType) ? 'subs' : 'in-app'; +} + +function getReceiptSku(receipt: VegaReceipt): string { + return receipt.sku ?? receipt.termSku ?? ''; +} + +function getCachedProductType( + receipt: VegaReceipt, + productTypesBySku: Map, + fallbackSku?: string, +): unknown { + const sku = getReceiptSku(receipt) || fallbackSku || ''; + return sku ? productTypesBySku.get(sku) : undefined; +} + +function productDataToArray( + productData?: Map | Record | null, +): VegaProduct[] { + if (!productData) return []; + if (productData instanceof Map) return Array.from(productData.values()); + return Object.values(productData); +} + +function createPricingPhase(product: VegaProduct) { + const price = product.price ?? {}; + return { + billingCycleCount: 0, + billingPeriod: product.subscriptionPeriod ?? '', + formattedPrice: price.priceStr ?? '', + priceAmountMicros: toPriceAmountMicros(price.valueInMicros), + priceCurrencyCode: price.priceCurrencyCode ?? '', + recurrenceMode: 1, + }; +} + +function createSubscriptionOffer(product: VegaProduct) { + const sku = product.sku ?? ''; + const pricingPhase = createPricingPhase(product); + return { + basePlanId: sku, + offerId: null, + offerTags: [], + offerToken: '', + pricingPhases: { + pricingPhaseList: [pricingPhase], + }, + }; +} + +function createStandardizedSubscriptionOffer(product: VegaProduct) { + const pricingPhase = createPricingPhase(product); + const sku = product.sku ?? ''; + return { + basePlanIdAndroid: sku, + currency: product.price?.priceCurrencyCode ?? '', + displayPrice: product.price?.priceStr ?? '', + id: sku, + offerTagsAndroid: [], + offerTokenAndroid: '', + paymentMode: 'pay-as-you-go' as const, + period: null, + price: microsToPrice(product.price?.valueInMicros) ?? 0, + pricingPhasesAndroid: { + pricingPhaseList: [pricingPhase], + }, + type: 'introductory' as const, + }; +} + +function mapProduct(product: VegaProduct): Product | ProductSubscription { + const sku = product.sku ?? ''; + const type = productTypeToOpenIap(product.productType); + const base = { + id: sku, + title: product.title ?? sku, + description: product.description ?? '', + displayName: product.title ?? sku, + displayPrice: product.price?.priceStr ?? '', + currency: product.price?.priceCurrencyCode ?? '', + price: microsToPrice(product.price?.valueInMicros), + debugDescription: null, + platform: 'android' as const, + nameAndroid: product.title ?? sku, + oneTimePurchaseOfferDetailsAndroid: null, + productStatusAndroid: 'ok' as const, + discountOffers: null, + }; + + if (type === 'subs') { + return { + ...base, + type, + subscriptionOfferDetailsAndroid: [createSubscriptionOffer(product)], + subscriptionOffers: [createStandardizedSubscriptionOffer(product)], + }; + } + + return { + ...base, + type, + subscriptionOfferDetailsAndroid: null, + subscriptionOffers: null, + }; +} + +function mapReceipt( + receipt: VegaReceipt, + fallbackProductType?: unknown, +): Purchase { + const receiptId = receipt.receiptId ?? ''; + const productId = getReceiptSku(receipt); + const type = productTypeToOpenIap(receipt.productType ?? fallbackProductType); + const isPending = Boolean(receipt.isDeferred); + const isCanceled = Boolean(receipt.isCancelled || receipt.cancelDate); + const isActive = !isCanceled && !isPending; + + return { + id: receiptId, + productId, + transactionDate: toTimestamp(receipt.purchaseDate), + purchaseToken: receiptId, + platform: 'android', + store: 'amazon', + quantity: 1, + purchaseState: isPending ? 'pending' : isActive ? 'purchased' : 'unknown', + isAutoRenewing: type === 'subs' && isActive, + transactionId: receiptId, + autoRenewingAndroid: type === 'subs' && isActive, + dataAndroid: JSON.stringify(receipt), + signatureAndroid: null, + isAcknowledgedAndroid: false, + packageNameAndroid: null, + obfuscatedAccountIdAndroid: null, + obfuscatedProfileIdAndroid: null, + developerPayloadAndroid: null, + isSuspendedAndroid: Boolean(receipt.isDeferred), + }; +} + +export function createExpoIapVegaModule( + service: VegaPurchasingService, +): ExpoIapVegaModule { + const productTypesBySku = new Map(); + const listenersByEventName = new Map>(); + let cachedUserData: VegaUserData | null = null; + + const emit = (eventName: string, payload: unknown): void => { + for (const listener of listenersByEventName.get(eventName) ?? []) { + listener(payload); + } + }; + + const getStorefront = async (): Promise => { + const response = await service.getUserData({}); + ensureSuccessful('user-data', response, 'Failed to fetch Amazon user data'); + cachedUserData = response.userData ?? null; + return cachedUserData?.marketplace ?? cachedUserData?.countryCode ?? ''; + }; + + const getPurchaseUpdateReceipts = async (): Promise => { + const receipts: VegaReceipt[] = []; + let reset = true; + let hasMore = false; + + do { + const response = await service.getPurchaseUpdates({reset}); + ensureSuccessful( + 'purchase-updates', + response, + 'Failed to fetch Amazon purchase updates', + ); + cachedUserData = response.userData ?? cachedUserData; + receipts.push(...(response.receiptList ?? [])); + hasMore = Boolean(response.hasMore); + reset = false; + } while (hasMore); + + return receipts; + }; + + const hydrateProductTypesForReceipts = async ( + receipts: VegaReceipt[], + ): Promise => { + const missingSkus = new Set(); + + for (const receipt of receipts) { + const sku = getReceiptSku(receipt); + if (!sku) continue; + if (receipt.productType != null) { + productTypesBySku.set(sku, receipt.productType); + } else if (!productTypesBySku.has(sku)) { + missingSkus.add(sku); + } + } + + if (missingSkus.size === 0) return; + + const response = await service.getProductData({ + skus: Array.from(missingSkus), + }); + ensureSuccessful( + 'product-data', + response, + 'Failed to fetch Amazon Vega product data for purchase updates', + ); + + for (const product of productDataToArray(response.productData)) { + if (product.sku) { + productTypesBySku.set(product.sku, product.productType); + } + } + }; + + const getAvailableItems = async ( + options?: PurchaseOptions, + ): Promise => { + const includeSuspended = Boolean(options?.includeSuspendedAndroid ?? false); + const receipts = await getPurchaseUpdateReceipts(); + await hydrateProductTypesForReceipts(receipts); + return receipts + .filter((receipt) => includeSuspended || !receipt.isDeferred) + .map((receipt) => + mapReceipt(receipt, getCachedProductType(receipt, productTypesBySku)), + ); + }; + + const finishReceipt = async (purchaseToken: string): Promise => { + if (!purchaseToken) { + throw createVegaError( + ErrorCode.DeveloperError, + 'purchaseToken is required to finish an Amazon Vega transaction.', + ); + } + + const response = await service.notifyFulfillment({ + fulfillmentResult: FULFILLMENT_RESULT_FULFILLED, + receiptId: purchaseToken, + }); + ensureSuccessful( + 'notify-fulfillment', + response, + 'Failed to notify Amazon Vega fulfillment', + ); + }; + + const verifyWithIapkit = async ( + options: VerifyPurchaseWithProviderProps, + ): Promise => { + function normalizeIapkitState(state: unknown): IapkitPurchaseState { + const normalized = + typeof state === 'string' + ? state.toLowerCase().replaceAll('_', '-') + : 'unknown'; + const states = new Set([ + 'entitled', + 'pending-acknowledgment', + 'pending', + 'canceled', + 'expired', + 'ready-to-consume', + 'consumed', + 'unknown', + 'inauthentic', + ]); + return states.has(normalized as IapkitPurchaseState) + ? (normalized as IapkitPurchaseState) + : 'unknown'; + } + + function extractIapkitErrorMessage(json: unknown): string | null { + if (!json || typeof json !== 'object') return null; + const record = json as Record; + const details = record.details; + if (details && typeof details === 'object') { + const originalError = (details as Record) + .originalError; + if (typeof originalError === 'string') { + try { + return ( + extractIapkitErrorMessage(JSON.parse(originalError)) ?? + originalError + ); + } catch { + return originalError; + } + } + } + + const errors = record.errors; + if (Array.isArray(errors) && errors.length > 0) { + return extractIapkitErrorMessage(errors[0]); + } + + return typeof record.message === 'string' + ? record.message + : typeof record.error === 'string' + ? record.error + : null; + } + + function parseIapkitJsonResponse(text: string): unknown | null { + if (!text.trim()) return null; + try { + return JSON.parse(text); + } catch { + return null; + } + } + + if (options.provider !== 'iapkit') { + throw createVegaError( + ErrorCode.FeatureNotSupported, + `Unsupported purchase verification provider: ${options.provider}.`, + ); + } + + const iapkit = options.iapkit; + const amazon = iapkit?.amazon; + if (!amazon) { + throw createVegaError( + ErrorCode.DeveloperError, + 'Amazon Vega IAPKit verification requires amazon parameters.', + ); + } + + const receiptId = amazon.receiptId.trim(); + if (!receiptId) { + throw createVegaError( + ErrorCode.DeveloperError, + 'Amazon Vega IAPKit verification requires amazon.receiptId.', + ); + } + + let userId = amazon.userId?.trim() ?? ''; + if (!userId) { + const response = await service.getUserData({}); + ensureSuccessful( + 'user-data', + response, + 'Failed to fetch Amazon user data for IAPKit verification', + ); + cachedUserData = response.userData ?? cachedUserData; + userId = cachedUserData?.userId?.trim() ?? ''; + } + if (!userId) { + throw createVegaError( + ErrorCode.DeveloperError, + 'Amazon Vega IAPKit verification could not resolve userId.', + ); + } + + let response: Response; + try { + response = await fetch(IAPKIT_VERIFY_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(iapkit?.apiKey + ? {Authorization: `Bearer ${iapkit.apiKey}`} + : {}), + }, + body: JSON.stringify({ + store: 'amazon', + userId, + receiptId, + ...(amazon.sandbox == null ? {} : {sandbox: amazon.sandbox}), + }), + }); + } catch (error) { + throw createVegaError( + ErrorCode.NetworkError, + error instanceof Error + ? error.message + : 'Failed to reach IAPKit verification endpoint.', + ); + } + const text = await response.text(); + const json = parseIapkitJsonResponse(text); + + if (!response.ok) { + throw createVegaError( + ErrorCode.ReceiptFailed, + extractIapkitErrorMessage(json) ?? `HTTP ${response.status}`, + ); + } + + if (json === null) { + throw createVegaError( + ErrorCode.ReceiptFailed, + `IAPKit returned non-JSON response (HTTP ${response.status}).`, + ); + } + + const result = json as { + isValid?: unknown; + state?: unknown; + store?: unknown; + }; + return { + provider: 'iapkit', + iapkit: { + isValid: result.isValid === true, + state: normalizeIapkitState(result.state), + store: 'amazon', + }, + }; + }; + + const vegaModule: ExpoIapVegaModule = { + ERROR_CODES: ErrorCode, + async initConnection(): Promise { + await getStorefront(); + return true; + }, + async endConnection(): Promise { + productTypesBySku.clear(); + cachedUserData = null; + listenersByEventName.clear(); + return true; + }, + async fetchProducts( + type: string, + skus: string[], + ): Promise<(Product | ProductSubscription)[]> { + if (!Array.isArray(skus) || skus.length === 0) { + throw createVegaError(ErrorCode.EmptySkuList, 'No SKUs provided'); + } + + const response = await service.getProductData({skus}); + ensureSuccessful( + 'product-data', + response, + 'Failed to fetch Amazon Vega products', + ); + + return productDataToArray(response.productData) + .filter((product) => { + if (product.sku) { + productTypesBySku.set(product.sku, product.productType); + } + const openIapType = productTypeToOpenIap(product.productType); + if (type === 'all') return true; + if (type === 'subs') return openIapType === 'subs'; + return openIapType === 'in-app'; + }) + .map(mapProduct); + }, + async requestPurchase(params): Promise { + let sku: string | undefined; + try { + const skus = params.skuArr ?? []; + if (skus.length !== 1) { + throw createVegaError( + ErrorCode.DeveloperError, + 'Amazon Vega purchase expects exactly one SKU per request.', + ); + } + + sku = skus[0]!; + const fallbackProductType = + params.type === 'subs' + ? PRODUCT_TYPE_SUBSCRIPTION + : productTypesBySku.get(sku); + if (fallbackProductType != null) { + productTypesBySku.set(sku, fallbackProductType); + } + const response = await service.purchase({sku}); + ensureSuccessful( + 'purchase', + response, + 'Failed to complete Amazon Vega purchase', + sku, + ); + cachedUserData = response.userData ?? cachedUserData; + + if (!response.receipt) return []; + const purchase = mapReceipt(response.receipt, fallbackProductType); + emit('purchase-updated', purchase); + return [purchase]; + } catch (error) { + emit( + 'purchase-error', + toPurchaseErrorPayload( + error, + 'Failed to complete Amazon Vega purchase', + sku, + ), + ); + throw error; + } + }, + getAvailableItems, + async getActiveSubscriptions( + subscriptionIds?: string[] | null, + ): Promise { + const requestedIds = new Set(subscriptionIds ?? []); + const purchases = await getAvailableItems({ + includeSuspendedAndroid: false, + }); + return purchases + .filter( + (purchase) => + purchase.isAutoRenewing && + (requestedIds.size === 0 || requestedIds.has(purchase.productId)), + ) + .map((purchase) => ({ + productId: purchase.productId, + isActive: true, + transactionId: purchase.transactionId ?? purchase.id, + purchaseToken: purchase.purchaseToken ?? null, + transactionDate: purchase.transactionDate, + expirationDateIOS: null, + environmentIOS: null, + willExpireSoon: null, + daysUntilExpirationIOS: null, + renewalInfoIOS: null, + autoRenewingAndroid: + purchase.platform === 'android' + ? (( + purchase as Purchase & { + autoRenewingAndroid?: boolean | null; + } + ).autoRenewingAndroid ?? null) + : null, + basePlanIdAndroid: null, + currentPlanId: null, + purchaseTokenAndroid: purchase.purchaseToken ?? null, + })); + }, + async hasActiveSubscriptions( + subscriptionIds?: string[] | null, + ): Promise { + const subscriptions = + await vegaModule.getActiveSubscriptions(subscriptionIds); + return subscriptions.length > 0; + }, + async acknowledgePurchaseAndroid(purchaseToken): Promise { + await finishReceipt(purchaseToken); + }, + async consumePurchaseAndroid(purchaseToken): Promise { + await finishReceipt(purchaseToken); + }, + async getStorefront(): Promise { + return getStorefront(); + }, + async verifyPurchaseWithProvider(options) { + return verifyWithIapkit(options); + }, + addListener(eventName, listener) { + const listeners = listenersByEventName.get(eventName) ?? new Set(); + listeners.add(listener); + listenersByEventName.set(eventName, listeners); + return { + remove: () => { + listeners.delete(listener); + }, + }; + }, + removeListener(eventName, listener): void { + listenersByEventName.get(eventName)?.delete(listener); + }, + }; + + return vegaModule; +} diff --git a/libraries/expo-iap/src/vega.kepler.ts b/libraries/expo-iap/src/vega.kepler.ts new file mode 100644 index 00000000..a07894fb --- /dev/null +++ b/libraries/expo-iap/src/vega.kepler.ts @@ -0,0 +1,24 @@ +import {Platform} from 'react-native'; +// eslint-disable-next-line import/no-unresolved +import {PurchasingService} from '@amazon-devices/keplerscript-appstore-iap-lib'; +import { + createExpoIapVegaModule, + type ExpoIapVegaModule, + type VegaPurchasingService, +} from './vega-adapter'; + +let cachedVegaModule: ExpoIapVegaModule | null = null; + +export const isVegaOS = (): boolean => { + return String(Platform.OS).toLowerCase() === 'kepler'; +}; + +export const getVegaIapModule = (): ExpoIapVegaModule | null => { + if (!isVegaOS()) return null; + if (!cachedVegaModule) { + cachedVegaModule = createExpoIapVegaModule( + PurchasingService as unknown as VegaPurchasingService, + ); + } + return cachedVegaModule; +}; diff --git a/libraries/expo-iap/src/vega.ts b/libraries/expo-iap/src/vega.ts new file mode 100644 index 00000000..90c26c64 --- /dev/null +++ b/libraries/expo-iap/src/vega.ts @@ -0,0 +1,10 @@ +import {Platform} from 'react-native'; +import type {ExpoIapVegaModule} from './vega-adapter'; + +export const isVegaOS = (): boolean => { + return String(Platform.OS).toLowerCase() === 'kepler'; +}; + +export const getVegaIapModule = (): ExpoIapVegaModule | null => { + return null; +}; diff --git a/libraries/flutter_inapp_purchase/android/build.gradle b/libraries/flutter_inapp_purchase/android/build.gradle index 9e853e87..c6c5bb82 100644 --- a/libraries/flutter_inapp_purchase/android/build.gradle +++ b/libraries/flutter_inapp_purchase/android/build.gradle @@ -134,17 +134,21 @@ android { } compileSdk = openIapCompileSdkVersion - // Read horizonEnabled from gradle.properties, default to false (play) def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false + def fireOsEnabled = project.findProperty('fireOsEnabled')?.toBoolean() ?: false + if (horizonEnabled && fireOsEnabled) { + throw new GradleException("flutter_inapp_purchase: horizonEnabled and fireOsEnabled cannot both be true") + } + def openiapFlavor = fireOsEnabled ? 'amazon' : (horizonEnabled ? 'horizon' : 'play') defaultConfig { minSdkVersion = openIapMinSdkVersion targetSdkVersion = openIapTargetSdkVersion testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - // Select the local :openiap flavor when developing inside the monorepo. - def flavor = horizonEnabled ? 'horizon' : 'play' - missingDimensionStrategy "platform", flavor + // Select the matching openiap-google flavor. + missingDimensionStrategy "platform", openiapFlavor + buildConfigField "String", "OPENIAP_STORE", "\"${openiapFlavor}\"" } sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -157,6 +161,9 @@ android { targetCompatibility JavaVersion.VERSION_17 } + buildFeatures { + buildConfig true + } } kotlin { @@ -168,9 +175,12 @@ kotlin { dependencies { // In monorepo: use local packages/google source if available def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false + def fireOsEnabled = project.findProperty('fireOsEnabled')?.toBoolean() ?: false def localGoogleProject = findProject(':openiap') if (localGoogleProject != null) { implementation project(':openiap') + } else if (fireOsEnabled) { + implementation "io.github.hyochan.openiap:openiap-google-amazon:${openiapGoogleVersion}" } else if (horizonEnabled) { implementation "io.github.hyochan.openiap:openiap-google-horizon:${openiapGoogleVersion}" } else { @@ -178,6 +188,7 @@ dependencies { } implementation "androidx.annotation:annotation:${readRequiredAndroidGradleProperty(projectDir, 'openIapAndroidAnnotationVersion')}" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0' // Google Play Billing comes transitively from openiap-google so its // version stays centralized in packages/google. diff --git a/libraries/flutter_inapp_purchase/android/settings.gradle b/libraries/flutter_inapp_purchase/android/settings.gradle index 961a10e4..308db5a9 100644 --- a/libraries/flutter_inapp_purchase/android/settings.gradle +++ b/libraries/flutter_inapp_purchase/android/settings.gradle @@ -1,7 +1,22 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "com.android.library" version "8.7.3" apply false + id "org.jetbrains.kotlin.android" version "2.2.0" apply false + id "org.jetbrains.kotlin.plugin.compose" version "2.2.0" apply false + id "com.vanniktech.maven.publish" version "0.34.0" apply false +} + rootProject.name = 'flutter_inapp_purchase' -// Optional: include the monorepo openiap-google module for debugging. -// android/build.gradle automatically uses project(":openiap") when it exists. -// -// include ':openiap' -// project(':openiap').projectDir = new File(settingsDir, '../../../packages/google/openiap') +def localOpenIapProject = new File(settingsDir, '../../../packages/google/openiap') +if (localOpenIapProject.exists()) { + include ':openiap' + project(':openiap').projectDir = localOpenIapProject +} diff --git a/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt b/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt index 1c587ab4..decd2fea 100644 --- a/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt +++ b/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt @@ -15,6 +15,7 @@ import dev.hyo.openiap.DeveloperBillingOptionParamsAndroid import dev.hyo.openiap.ExternalLinkLaunchModeAndroid import dev.hyo.openiap.ExternalLinkTypeAndroid import dev.hyo.openiap.FetchProductsResult +import dev.hyo.openiap.FetchProductsResultAll import dev.hyo.openiap.FetchProductsResultProducts import dev.hyo.openiap.FetchProductsResultSubscriptions import dev.hyo.openiap.InitConnectionConfig @@ -87,11 +88,12 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act deduplicate: Boolean = false ): JSONArray { val entries: List> = when (result) { + is FetchProductsResultAll -> result.value?.map { it.toJson() } + ?: emptyList() is FetchProductsResultProducts -> result.value?.map { it.toJson() } ?: emptyList() is FetchProductsResultSubscriptions -> result.value?.map { it.toJson() } ?: emptyList() - else -> emptyList>() } val array = JSONArray() val seenIds = mutableSetOf() @@ -247,6 +249,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act } } + @Suppress("DEPRECATION") override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { val ch = channel if (ch == null) { diff --git a/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/FlutterInappPurchasePlugin.kt b/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/FlutterInappPurchasePlugin.kt index 98cb0cfd..08458dfb 100644 --- a/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/FlutterInappPurchasePlugin.kt +++ b/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/FlutterInappPurchasePlugin.kt @@ -20,7 +20,8 @@ class FlutterInappPurchasePlugin : FlutterPlugin, ActivityAware { private fun onAttached(context: Context, messenger: BinaryMessenger) { val methodChannel = MethodChannel(messenger, "flutter_inapp") channel = methodChannel - logInfo("Initializing Android IAP plugin") + configuredStore = BuildConfig.OPENIAP_STORE + logInfo("Initializing Android IAP plugin for ${configuredStore} store") val plugin = AndroidInappPurchasePlugin() plugin.setContext(context) plugin.setChannel(methodChannel) @@ -54,6 +55,7 @@ class FlutterInappPurchasePlugin : FlutterPlugin, ActivityAware { companion object { private const val TAG = "FlutterInappPurchase" + private var configuredStore = "play" private fun logInfo(message: String) { if (Log.isLoggable(TAG, Log.INFO)) { @@ -62,7 +64,11 @@ class FlutterInappPurchasePlugin : FlutterPlugin, ActivityAware { } fun getStore(): String { - return "play_store" + return when (configuredStore) { + "amazon" -> "amazon" + "horizon" -> "horizon" + else -> "play_store" + } } } } diff --git a/libraries/flutter_inapp_purchase/example/README.md b/libraries/flutter_inapp_purchase/example/README.md index 4bf93883..cdcfc250 100644 --- a/libraries/flutter_inapp_purchase/example/README.md +++ b/libraries/flutter_inapp_purchase/example/README.md @@ -18,6 +18,7 @@ This example supports multiple billing platforms: - **Google Play Billing** (default) - **Meta Horizon Billing** (for Meta Quest devices) +- **Fire OS IAP** (Amazon Appstore distribution) ### Google Play (Default) @@ -33,11 +34,13 @@ flutter build apk --release To use Meta Horizon billing: 1. **Enable Horizon** in `android/gradle.properties`: + ```properties horizonEnabled=true ``` 2. **Add Horizon App ID** to `android/local.properties`: + ```properties HORIZON_APP_ID=your_horizon_app_id_here ``` @@ -48,13 +51,38 @@ To use Meta Horizon billing: flutter build apk --release ``` -**No flavor specification needed!** The build system automatically selects the correct billing platform based on `horizonEnabled`. +**No flavor specification needed!** The build system automatically selects the correct billing platform based on `horizonEnabled` or `fireOsEnabled`. + +### Fire OS + +To use Fire OS IAP through the Amazon Appstore SDK: + +1. **Enable Fire OS** in `android/gradle.properties`: + + ```properties + fireOsEnabled=true + ``` + +2. **Keep Horizon disabled** in the same build: + + ```properties + horizonEnabled=false + ``` + +3. **Test with Amazon App Tester** on a Fire OS or compatible Android test device: + ```bash + flutter run + flutter build apk --release + ``` + +The build system automatically selects the Fire OS `amazon` flavor based on +`fireOsEnabled`. ## IDE Configuration ### Android Studio -Just click **Run** - the build system automatically selects the right platform based on `horizonEnabled` in `gradle.properties`. +Just click **Run** - the build system automatically selects the right platform based on `horizonEnabled` or `fireOsEnabled` in `gradle.properties`. ### VS Code @@ -64,3 +92,4 @@ Press F5 or click **Start Debugging** - works out of the box! - **Google Play**: Test on any Android device with Google Play Store (default) - **Meta Horizon**: Set `horizonEnabled=true` and test on Meta Quest devices +- **Fire OS**: Set `fireOsEnabled=true` and test with Amazon App Tester diff --git a/libraries/flutter_inapp_purchase/example/android/app/build.gradle b/libraries/flutter_inapp_purchase/example/android/app/build.gradle index 3e3c4168..d67b81b0 100644 --- a/libraries/flutter_inapp_purchase/example/android/app/build.gradle +++ b/libraries/flutter_inapp_purchase/example/android/app/build.gradle @@ -56,9 +56,13 @@ android { versionName = flutterVersionName testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - // Read horizonEnabled flag from gradle.properties (default: false) + // Read store flags from gradle.properties (default: Play) def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false - def flavor = horizonEnabled ? 'horizon' : 'play' + def fireOsEnabled = project.findProperty('fireOsEnabled')?.toBoolean() ?: false + if (horizonEnabled && fireOsEnabled) { + throw new GradleException("flutter_inapp_purchase example: horizonEnabled and fireOsEnabled cannot both be true") + } + def flavor = fireOsEnabled ? 'amazon' : (horizonEnabled ? 'horizon' : 'play') // Select platform flavor from plugin's product flavors missingDimensionStrategy 'platform', flavor diff --git a/libraries/flutter_inapp_purchase/example/android/gradle.properties b/libraries/flutter_inapp_purchase/example/android/gradle.properties index 5a000b5b..d61db4c5 100644 --- a/libraries/flutter_inapp_purchase/example/android/gradle.properties +++ b/libraries/flutter_inapp_purchase/example/android/gradle.properties @@ -11,3 +11,8 @@ openIapEspressoCoreVersion=3.5.1 # Default: false (uses Google Play Billing) # Uncomment to enable Horizon billing: # horizonEnabled=true + +# Enable Fire OS support for Amazon distribution +# Default: false (uses Google Play Billing unless horizonEnabled=true) +# Do not enable with horizonEnabled in the same build. +# fireOsEnabled=true diff --git a/libraries/flutter_inapp_purchase/example/android/settings.gradle b/libraries/flutter_inapp_purchase/example/android/settings.gradle index 543fd7b0..c329a22a 100644 --- a/libraries/flutter_inapp_purchase/example/android/settings.gradle +++ b/libraries/flutter_inapp_purchase/example/android/settings.gradle @@ -34,9 +34,15 @@ pluginManagement { def androidGradlePluginVersion = googlePluginVersion('com.android.application') ?: readGradleProperty('openIapAndroidGradlePluginVersion') ?: '8.13.2' + def androidLibraryPluginVersion = googlePluginVersion('com.android.library') + ?: androidGradlePluginVersion def kotlinPluginVersion = googlePluginVersion('org.jetbrains.kotlin.android') ?: readGradleProperty('openIapKotlinVersion') ?: '2.2.0' + def composePluginVersion = googlePluginVersion('org.jetbrains.kotlin.plugin.compose') + ?: kotlinPluginVersion + def vanniktechPluginVersion = googlePluginVersion('com.vanniktech.maven.publish') + ?: '0.35.0' def flutterSdkPath = { def properties = new Properties() @@ -59,9 +65,18 @@ pluginManagement { if (requested.id.id == "com.android.application") { useVersion(androidGradlePluginVersion) } + if (requested.id.id == "com.android.library") { + useVersion(androidLibraryPluginVersion) + } if (requested.id.id == "org.jetbrains.kotlin.android") { useVersion(kotlinPluginVersion) } + if (requested.id.id == "org.jetbrains.kotlin.plugin.compose") { + useVersion(composePluginVersion) + } + if (requested.id.id == "com.vanniktech.maven.publish") { + useVersion(vanniktechPluginVersion) + } } } } @@ -69,7 +84,16 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" apply false + id "com.android.library" apply false id "org.jetbrains.kotlin.android" apply false + id "org.jetbrains.kotlin.plugin.compose" apply false + id "com.vanniktech.maven.publish" apply false } include ':app' + +def localOpenIapProject = new File(settingsDir, '../../../../packages/google/openiap') +if (localOpenIapProject.exists()) { + include ':openiap' + project(':openiap').projectDir = localOpenIapProject +} diff --git a/libraries/flutter_inapp_purchase/lib/types.dart b/libraries/flutter_inapp_purchase/lib/types.dart index 9723c028..1f93a65f 100644 --- a/libraries/flutter_inapp_purchase/lib/types.dart +++ b/libraries/flutter_inapp_purchase/lib/types.dart @@ -534,7 +534,8 @@ enum IapStore { Unknown('unknown'), Apple('apple'), Google('google'), - Horizon('horizon'); + Horizon('horizon'), + Amazon('amazon'); const IapStore(this.value); final String value; @@ -550,6 +551,8 @@ enum IapStore { return IapStore.Google; case 'horizon': return IapStore.Horizon; + case 'amazon': + return IapStore.Amazon; } throw ArgumentError('Unknown IapStore value: $value'); } @@ -4540,7 +4543,8 @@ class _SubsPurchase extends RequestPurchaseProps { /// /// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. /// - apple: Always targets App Store -/// - google: Targets Play Store by default, or Horizon when built with horizon flavor +/// - google: Targets Play Store by default, Horizon when built with horizon flavor, +/// or Fire OS when built with amazon flavor /// (determined at build time, not runtime) class RequestPurchasePropsByPlatforms { const RequestPurchasePropsByPlatforms({ @@ -4717,7 +4721,8 @@ class RequestSubscriptionIosProps { /// /// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. /// - apple: Always targets App Store -/// - google: Targets Play Store by default, or Horizon when built with horizon flavor +/// - google: Targets Play Store by default, Horizon when built with horizon flavor, +/// or Fire OS when built with amazon flavor /// (determined at build time, not runtime) class RequestSubscriptionPropsByPlatforms { const RequestSubscriptionPropsByPlatforms({ @@ -4755,6 +4760,37 @@ class RequestSubscriptionPropsByPlatforms { } } +class RequestVerifyPurchaseWithIapkitAmazonProps { + const RequestVerifyPurchaseWithIapkitAmazonProps({ + required this.receiptId, + this.sandbox, + this.userId, + }); + + /// Amazon Appstore receipt id returned by PurchaseResponse.getReceipt().getReceiptId(). + final String receiptId; + /// Use Amazon RVS Cloud Sandbox for App Tester receipts. + final bool? sandbox; + /// Amazon Appstore user id returned by PurchaseResponse.getUserData().getUserId(). + final String? userId; + + factory RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(Map json) { + return RequestVerifyPurchaseWithIapkitAmazonProps( + receiptId: json['receiptId'] as String, + sandbox: json['sandbox'] as bool?, + userId: json['userId'] as String?, + ); + } + + Map toJson() { + return { + 'receiptId': receiptId, + 'sandbox': sandbox, + 'userId': userId, + }; + } +} + class RequestVerifyPurchaseWithIapkitAppleProps { const RequestVerifyPurchaseWithIapkitAppleProps({ required this.jws, @@ -4801,13 +4837,17 @@ class RequestVerifyPurchaseWithIapkitGoogleProps { /// /// - apple: Verifies via App Store (JWS token) /// - google: Verifies via Play Store (purchase token) +/// - amazon: Verifies via Amazon Appstore RVS (userId + receiptId) class RequestVerifyPurchaseWithIapkitProps { const RequestVerifyPurchaseWithIapkitProps({ + this.amazon, this.apiKey, this.apple, this.google, }); + /// Amazon Appstore verification parameters. + final RequestVerifyPurchaseWithIapkitAmazonProps? amazon; /// API key used for the Authorization header (Bearer {apiKey}). final String? apiKey; /// Apple App Store verification parameters. @@ -4817,6 +4857,7 @@ class RequestVerifyPurchaseWithIapkitProps { factory RequestVerifyPurchaseWithIapkitProps.fromJson(Map json) { return RequestVerifyPurchaseWithIapkitProps( + amazon: json['amazon'] != null ? RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(json['amazon'] as Map) : null, apiKey: json['apiKey'] as String?, apple: json['apple'] != null ? RequestVerifyPurchaseWithIapkitAppleProps.fromJson(json['apple'] as Map) : null, google: json['google'] != null ? RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(json['google'] as Map) : null, @@ -4825,6 +4866,7 @@ class RequestVerifyPurchaseWithIapkitProps { Map toJson() { return { + 'amazon': amazon?.toJson(), 'apiKey': apiKey, 'apple': apple?.toJson(), 'google': google?.toJson(), diff --git a/libraries/flutter_inapp_purchase/pubspec.yaml b/libraries/flutter_inapp_purchase/pubspec.yaml index cc1f7ee3..c62cecd1 100644 --- a/libraries/flutter_inapp_purchase/pubspec.yaml +++ b/libraries/flutter_inapp_purchase/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_inapp_purchase description: In App Purchase plugin for flutter. This project has been forked by react-native-iap and we are willing to share same experience with that on react-native. -version: 9.3.2 +version: 9.4.0-rc.1 homepage: https://github.com/hyodotdev/openiap/tree/main/libraries/flutter_inapp_purchase repository: https://github.com/hyodotdev/openiap/tree/main/libraries/flutter_inapp_purchase environment: diff --git a/libraries/godot-iap/addons/godot-iap/plugin.cfg b/libraries/godot-iap/addons/godot-iap/plugin.cfg index 164d8178..28b252f5 100644 --- a/libraries/godot-iap/addons/godot-iap/plugin.cfg +++ b/libraries/godot-iap/addons/godot-iap/plugin.cfg @@ -3,5 +3,5 @@ name="GodotIap" description="Cross-platform in-app purchase plugin for Godot using OpenIAP" author="hyochan" -version="2.3.1" +version="2.4.0-rc.1" script="godot_iap_plugin.gd" diff --git a/libraries/godot-iap/addons/godot-iap/types.gd b/libraries/godot-iap/addons/godot-iap/types.gd index 76bc09e7..802cf309 100644 --- a/libraries/godot-iap/addons/godot-iap/types.gd +++ b/libraries/godot-iap/addons/godot-iap/types.gd @@ -182,6 +182,7 @@ enum IapStore { APPLE = 1, GOOGLE = 2, HORIZON = 3, + AMAZON = 4, } ## Payment mode for subscription offers. Determines how the user pays during the offer period. @@ -4029,7 +4030,7 @@ class RequestPurchaseProps: dict["useAlternativeBilling"] = use_alternative_billing return dict -## Platform-specific purchase request parameters. Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. - apple: Always targets App Store - google: Targets Play Store by default, or Horizon when built with horizon flavor (determined at build time, not runtime) +## Platform-specific purchase request parameters. Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. - apple: Always targets App Store - google: Targets Play Store by default, Horizon when built with horizon flavor, or Fire OS when built with amazon flavor (determined at build time, not runtime) class RequestPurchasePropsByPlatforms: ## Apple-specific purchase parameters var apple: RequestPurchaseIosProps @@ -4254,7 +4255,7 @@ class RequestSubscriptionIosProps: dict["advancedCommerceData"] = advanced_commerce_data return dict -## Platform-specific subscription request parameters. Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. - apple: Always targets App Store - google: Targets Play Store by default, or Horizon when built with horizon flavor (determined at build time, not runtime) +## Platform-specific subscription request parameters. Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. - apple: Always targets App Store - google: Targets Play Store by default, Horizon when built with horizon flavor, or Fire OS when built with amazon flavor (determined at build time, not runtime) class RequestSubscriptionPropsByPlatforms: ## Apple-specific subscription parameters var apple: RequestSubscriptionIosProps @@ -4313,6 +4314,34 @@ class RequestSubscriptionPropsByPlatforms: dict["android"] = android return dict +class RequestVerifyPurchaseWithIapkitAmazonProps: + ## Amazon Appstore user id returned by PurchaseResponse.getUserData().getUserId(). + var user_id: Variant = null + ## Amazon Appstore receipt id returned by PurchaseResponse.getReceipt().getReceiptId(). + var receipt_id: String = "" + ## Use Amazon RVS Cloud Sandbox for App Tester receipts. + var sandbox: Variant = null + + static func from_dict(data: Dictionary) -> RequestVerifyPurchaseWithIapkitAmazonProps: + var obj = RequestVerifyPurchaseWithIapkitAmazonProps.new() + if data.has("userId") and data["userId"] != null: + obj.user_id = data["userId"] + if data.has("receiptId") and data["receiptId"] != null: + obj.receipt_id = data["receiptId"] + if data.has("sandbox") and data["sandbox"] != null: + obj.sandbox = data["sandbox"] + return obj + + func to_dict() -> Dictionary: + var dict = {} + if user_id != null: + dict["userId"] = user_id + if receipt_id != null: + dict["receiptId"] = receipt_id + if sandbox != null: + dict["sandbox"] = sandbox + return dict + class RequestVerifyPurchaseWithIapkitAppleProps: ## The JWS token returned with the purchase response. var jws: String = "" @@ -4345,7 +4374,7 @@ class RequestVerifyPurchaseWithIapkitGoogleProps: dict["purchaseToken"] = purchase_token return dict -## Platform-specific verification parameters for IAPKit. - apple: Verifies via App Store (JWS token) - google: Verifies via Play Store (purchase token) +## Platform-specific verification parameters for IAPKit. - apple: Verifies via App Store (JWS token) - google: Verifies via Play Store (purchase token) - amazon: Verifies via Amazon Appstore RVS (userId + receiptId) class RequestVerifyPurchaseWithIapkitProps: ## API key used for the Authorization header (Bearer {apiKey}). var api_key: Variant = null @@ -4353,6 +4382,8 @@ class RequestVerifyPurchaseWithIapkitProps: var apple: RequestVerifyPurchaseWithIapkitAppleProps ## Google Play Store verification parameters. var google: RequestVerifyPurchaseWithIapkitGoogleProps + ## Amazon Appstore verification parameters. + var amazon: RequestVerifyPurchaseWithIapkitAmazonProps static func from_dict(data: Dictionary) -> RequestVerifyPurchaseWithIapkitProps: var obj = RequestVerifyPurchaseWithIapkitProps.new() @@ -4368,6 +4399,11 @@ class RequestVerifyPurchaseWithIapkitProps: obj.google = RequestVerifyPurchaseWithIapkitGoogleProps.from_dict(data["google"]) else: obj.google = data["google"] + if data.has("amazon") and data["amazon"] != null: + if data["amazon"] is Dictionary: + obj.amazon = RequestVerifyPurchaseWithIapkitAmazonProps.from_dict(data["amazon"]) + else: + obj.amazon = data["amazon"] return obj func to_dict() -> Dictionary: @@ -4384,6 +4420,11 @@ class RequestVerifyPurchaseWithIapkitProps: dict["google"] = google.to_dict() else: dict["google"] = google + if amazon != null: + if amazon.has_method("to_dict"): + dict["amazon"] = amazon.to_dict() + else: + dict["amazon"] = amazon return dict ## Product-level subscription replacement parameters (Android) Used with setSubscriptionProductReplacementParams in BillingFlowParams.ProductDetailsParams Available in Google Play Billing Library 8.1.0+ @@ -4728,7 +4769,8 @@ const IAP_STORE_VALUES = { IapStore.UNKNOWN: "unknown", IapStore.APPLE: "apple", IapStore.GOOGLE: "google", - IapStore.HORIZON: "horizon" + IapStore.HORIZON: "horizon", + IapStore.AMAZON: "amazon" } const PAYMENT_MODE_VALUES = { @@ -4997,7 +5039,8 @@ const IAP_STORE_FROM_STRING = { "unknown": IapStore.UNKNOWN, "apple": IapStore.APPLE, "google": IapStore.GOOGLE, - "horizon": IapStore.HORIZON + "horizon": IapStore.HORIZON, + "amazon": IapStore.AMAZON } const PAYMENT_MODE_FROM_STRING = { diff --git a/libraries/kmp-iap/gradle.properties b/libraries/kmp-iap/gradle.properties index 2deac08a..deb4ce9b 100644 --- a/libraries/kmp-iap/gradle.properties +++ b/libraries/kmp-iap/gradle.properties @@ -8,7 +8,7 @@ kotlin.apple.xcodeCompatibility.nowarn=true #MPP kotlin.mpp.enableCInteropCommonization=true #Version -libraryVersion=2.3.1 +libraryVersion=2.4.0-rc.1 #Android android.useAndroidX=true android.nonTransitiveRClass=true diff --git a/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt b/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt index 6e4ffda4..5abcc05c 100644 --- a/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt +++ b/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt @@ -629,7 +629,8 @@ public enum class IapStore(val rawValue: String) { Unknown("unknown"), Apple("apple"), Google("google"), - Horizon("horizon"); + Horizon("horizon"), + Amazon("amazon"); companion object { fun fromJson(value: String): IapStore = when (value) { @@ -645,6 +646,9 @@ public enum class IapStore(val rawValue: String) { "horizon" -> IapStore.Horizon "HORIZON" -> IapStore.Horizon "Horizon" -> IapStore.Horizon + "amazon" -> IapStore.Amazon + "AMAZON" -> IapStore.Amazon + "Amazon" -> IapStore.Amazon else -> throw IllegalArgumentException("Unknown IapStore value: $value") } } @@ -4621,7 +4625,8 @@ public data class RequestPurchaseProps( * * Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. * - apple: Always targets App Store - * - google: Targets Play Store by default, or Horizon when built with horizon flavor + * - google: Targets Play Store by default, Horizon when built with horizon flavor, + * or Fire OS when built with amazon flavor * (determined at build time, not runtime) */ public data class RequestPurchasePropsByPlatforms( @@ -4825,7 +4830,8 @@ public data class RequestSubscriptionIosProps( * * Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. * - apple: Always targets App Store - * - google: Targets Play Store by default, or Horizon when built with horizon flavor + * - google: Targets Play Store by default, Horizon when built with horizon flavor, + * or Fire OS when built with amazon flavor * (determined at build time, not runtime) */ public data class RequestSubscriptionPropsByPlatforms( @@ -4865,6 +4871,41 @@ public data class RequestSubscriptionPropsByPlatforms( ) } +public data class RequestVerifyPurchaseWithIapkitAmazonProps( + /** + * Amazon Appstore receipt id returned by PurchaseResponse.getReceipt().getReceiptId(). + */ + val receiptId: String, + /** + * Use Amazon RVS Cloud Sandbox for App Tester receipts. + */ + val sandbox: Boolean? = null, + /** + * Amazon Appstore user id returned by PurchaseResponse.getUserData().getUserId(). + */ + val userId: String? = null +) { + companion object { + fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitAmazonProps? { + val receiptId = json["receiptId"] as? String + val sandbox = json["sandbox"] as? Boolean + val userId = json["userId"] as? String + if (receiptId == null) return null + return RequestVerifyPurchaseWithIapkitAmazonProps( + receiptId = receiptId, + sandbox = sandbox, + userId = userId, + ) + } + } + + fun toJson(): Map = mapOf( + "receiptId" to receiptId, + "sandbox" to sandbox, + "userId" to userId, + ) +} + public data class RequestVerifyPurchaseWithIapkitAppleProps( /** * The JWS token returned with the purchase response. @@ -4912,8 +4953,13 @@ public data class RequestVerifyPurchaseWithIapkitGoogleProps( * * - apple: Verifies via App Store (JWS token) * - google: Verifies via Play Store (purchase token) + * - amazon: Verifies via Amazon Appstore RVS (userId + receiptId) */ public data class RequestVerifyPurchaseWithIapkitProps( + /** + * Amazon Appstore verification parameters. + */ + val amazon: RequestVerifyPurchaseWithIapkitAmazonProps? = null, /** * API key used for the Authorization header (Bearer {apiKey}). */ @@ -4930,6 +4976,7 @@ public data class RequestVerifyPurchaseWithIapkitProps( companion object { fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitProps { return RequestVerifyPurchaseWithIapkitProps( + amazon = (json["amazon"] as? Map)?.let { RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(it) }, apiKey = json["apiKey"] as? String, apple = (json["apple"] as? Map)?.let { RequestVerifyPurchaseWithIapkitAppleProps.fromJson(it) }, google = (json["google"] as? Map)?.let { RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(it) }, @@ -4938,6 +4985,7 @@ public data class RequestVerifyPurchaseWithIapkitProps( } fun toJson(): Map = mapOf( + "amazon" to amazon?.toJson(), "apiKey" to apiKey, "apple" to apple?.toJson(), "google" to google?.toJson(), diff --git a/libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj b/libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj index 92b6fc3f..c9aadfb1 100644 --- a/libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj +++ b/libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj @@ -21,7 +21,7 @@ OpenIap.Maui OpenIap.Maui OpenIap.Maui - 1.1.1 + 1.2.0-rc.1 OpenIAP for .NET MAUI OpenIAP — unified in-app purchases for .NET MAUI (iOS / Android). Mirrors the OpenIAP GraphQL schema and shares the same API surface as react-native-iap, expo-iap, flutter_inapp_purchase, and kmp-iap. hyodotdev diff --git a/libraries/maui-iap/src/OpenIap.Maui/Types.cs b/libraries/maui-iap/src/OpenIap.Maui/Types.cs index c35b1bd3..05aab794 100644 --- a/libraries/maui-iap/src/OpenIap.Maui/Types.cs +++ b/libraries/maui-iap/src/OpenIap.Maui/Types.cs @@ -978,7 +978,8 @@ public enum IapStore Unknown, Apple, Google, - Horizon + Horizon, + Amazon } public sealed class IapStoreJsonConverter : JsonConverter @@ -997,6 +998,9 @@ public sealed class IapStoreJsonConverter : JsonConverter ["horizon"] = IapStore.Horizon, ["HORIZON"] = IapStore.Horizon, ["Horizon"] = IapStore.Horizon, + ["amazon"] = IapStore.Amazon, + ["AMAZON"] = IapStore.Amazon, + ["Amazon"] = IapStore.Amazon, }; private static readonly Dictionary _toString = new() @@ -1005,6 +1009,7 @@ public sealed class IapStoreJsonConverter : JsonConverter [IapStore.Apple] = "apple", [IapStore.Google] = "google", [IapStore.Horizon] = "horizon", + [IapStore.Amazon] = "amazon", }; public override IapStore Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -3846,7 +3851,8 @@ public sealed record RequestPurchaseProps /// /// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. /// - apple: Always targets App Store -/// - google: Targets Play Store by default, or Horizon when built with horizon flavor +/// - google: Targets Play Store by default, Horizon when built with horizon flavor, +/// or Fire OS when built with amazon flavor /// (determined at build time, not runtime) public sealed record RequestPurchasePropsByPlatforms { @@ -3943,7 +3949,8 @@ public sealed record RequestSubscriptionIosProps /// /// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. /// - apple: Always targets App Store -/// - google: Targets Play Store by default, or Horizon when built with horizon flavor +/// - google: Targets Play Store by default, Horizon when built with horizon flavor, +/// or Fire OS when built with amazon flavor /// (determined at build time, not runtime) public sealed record RequestSubscriptionPropsByPlatforms { @@ -3961,6 +3968,19 @@ public sealed record RequestSubscriptionPropsByPlatforms public RequestSubscriptionAndroidProps? Android { get; init; } } +public sealed record RequestVerifyPurchaseWithIapkitAmazonProps +{ + /// Amazon Appstore user id returned by PurchaseResponse.getUserData().getUserId(). + [JsonPropertyName("userId")] + public string? UserId { get; init; } + /// Amazon Appstore receipt id returned by PurchaseResponse.getReceipt().getReceiptId(). + [JsonPropertyName("receiptId")] + public required string ReceiptId { get; init; } + /// Use Amazon RVS Cloud Sandbox for App Tester receipts. + [JsonPropertyName("sandbox")] + public bool? Sandbox { get; init; } +} + public sealed record RequestVerifyPurchaseWithIapkitAppleProps { /// The JWS token returned with the purchase response. @@ -3979,6 +3999,7 @@ public sealed record RequestVerifyPurchaseWithIapkitGoogleProps /// /// - apple: Verifies via App Store (JWS token) /// - google: Verifies via Play Store (purchase token) +/// - amazon: Verifies via Amazon Appstore RVS (userId + receiptId) public sealed record RequestVerifyPurchaseWithIapkitProps { /// API key used for the Authorization header (Bearer {apiKey}). @@ -3990,6 +4011,9 @@ public sealed record RequestVerifyPurchaseWithIapkitProps /// Google Play Store verification parameters. [JsonPropertyName("google")] public RequestVerifyPurchaseWithIapkitGoogleProps? Google { get; init; } + /// Amazon Appstore verification parameters. + [JsonPropertyName("amazon")] + public RequestVerifyPurchaseWithIapkitAmazonProps? Amazon { get; init; } } /// Product-level subscription replacement parameters (Android) diff --git a/libraries/react-native-iap/README.md b/libraries/react-native-iap/README.md index 9daa6d56..ecb43f6f 100644 --- a/libraries/react-native-iap/README.md +++ b/libraries/react-native-iap/README.md @@ -105,7 +105,7 @@ Before installing React Native IAP, make sure you have: #### Android Configuration -**Kotlin Version Requirement:** This library requires Kotlin 2.0+. Configure your project's Kotlin version: +**Kotlin Version Requirement:** This library requires Kotlin 2.2+. Configure your project's Kotlin version: In your root `android/build.gradle`: diff --git a/libraries/react-native-iap/android/build.gradle b/libraries/react-native-iap/android/build.gradle index 28c4de04..96147102 100644 --- a/libraries/react-native-iap/android/build.gradle +++ b/libraries/react-native-iap/android/build.gradle @@ -107,8 +107,13 @@ def getExtOrIntegerDefault(name) { return getExtOrDefault(name).toString().toInteger() } -// Read horizonEnabled from gradle.properties, default to false (play) +// Read store flags from gradle.properties, default to play def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false +def fireOsEnabled = project.findProperty('fireOsEnabled')?.toBoolean() ?: false +if (horizonEnabled && fireOsEnabled) { + throw new GradleException("react-native-iap: horizonEnabled and fireOsEnabled cannot both be true") +} +def openiapFlavor = fireOsEnabled ? 'amazon' : (horizonEnabled ? 'horizon' : 'play') def resolveOpenIapGoogleBuildFile() { def candidates = [ @@ -155,9 +160,8 @@ android { // Ship consumer keep rules so Nitro HybridObjects aren't stripped in app release builds consumerProguardFiles 'consumer-rules.pro' - // Use horizonEnabled to determine platform flavor - def flavor = horizonEnabled ? 'horizon' : 'play' - missingDimensionStrategy "platform", flavor + // Use explicit store flags to determine platform flavor + missingDimensionStrategy "platform", openiapFlavor externalNativeBuild { cmake { @@ -251,7 +255,9 @@ dependencies { } // Google Play Services - implementation "com.google.android.gms:play-services-base:$playServicesBaseVersion" + if (!fireOsEnabled && !horizonEnabled) { + implementation "com.google.android.gms:play-services-base:$playServicesBaseVersion" + } // Kotlin coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" @@ -261,6 +267,8 @@ dependencies { def localGoogleProject = findProject(':openiap') if (localGoogleProject != null) { implementation project(':openiap') + } else if (fireOsEnabled) { + implementation "io.github.hyochan.openiap:openiap-google-amazon:${googleVersionString}" } else if (horizonEnabled) { implementation "io.github.hyochan.openiap:openiap-google-horizon:${googleVersionString}" } else { diff --git a/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt b/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt index 390e34fd..a347cb5a 100644 --- a/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +++ b/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt @@ -1275,11 +1275,12 @@ class HybridRnIap : HybridRnIapSpec() { } private fun mapIapStore(store: dev.hyo.openiap.IapStore): IapStore { - return when (store) { - dev.hyo.openiap.IapStore.Apple -> IapStore.APPLE - dev.hyo.openiap.IapStore.Google -> IapStore.GOOGLE - dev.hyo.openiap.IapStore.Horizon -> IapStore.HORIZON - dev.hyo.openiap.IapStore.Unknown -> IapStore.UNKNOWN + return when (store.rawValue.lowercase()) { + "apple" -> IapStore.APPLE + "google" -> IapStore.GOOGLE + "horizon" -> IapStore.HORIZON + "amazon" -> IapStore.AMAZON + else -> IapStore.UNKNOWN } } @@ -1482,6 +1483,14 @@ class HybridRnIap : HybridRnIapSpec() { (iapkit.google as? Variant_NullType_NitroVerifyPurchaseWithIapkitGoogleProps.Second)?.value?.let { google -> iapkitMap["google"] = mapOf("purchaseToken" to google.purchaseToken) } + (iapkit.amazon as? Variant_NullType_NitroVerifyPurchaseWithIapkitAmazonProps.Second)?.value?.let { amazon -> + val amazonMap = mutableMapOf( + "receiptId" to amazon.receiptId + ) + amazon.userId.unwrapString()?.let { amazonMap["userId"] = it } + amazon.sandbox.unwrapBool()?.let { amazonMap["sandbox"] = it } + iapkitMap["amazon"] = amazonMap + } (iapkit.apple as? Variant_NullType_NitroVerifyPurchaseWithIapkitAppleProps.Second)?.value?.let { apple -> iapkitMap["apple"] = mapOf("jws" to apple.jws) } @@ -1609,6 +1618,7 @@ class HybridRnIap : HybridRnIapSpec() { // Alternative Billing (Android) // ------------------------------------------------------------------------- + @Suppress("DEPRECATION") override fun checkAlternativeBillingAvailabilityAndroid(): Promise { return Promise.async { RnIapLog.payload("checkAlternativeBillingAvailabilityAndroid", null) @@ -1626,6 +1636,7 @@ class HybridRnIap : HybridRnIapSpec() { } } + @Suppress("DEPRECATION") override fun showAlternativeBillingDialogAndroid(): Promise { return Promise.async { RnIapLog.payload("showAlternativeBillingDialogAndroid", null) @@ -1647,6 +1658,7 @@ class HybridRnIap : HybridRnIapSpec() { } } + @Suppress("DEPRECATION") override fun createAlternativeBillingTokenAndroid(sku: Variant_NullType_String?): Promise { return Promise.async { val skuValue = sku.unwrapString() @@ -2000,6 +2012,7 @@ class HybridRnIap : HybridRnIapSpec() { "APPLE" -> IapStore.APPLE "GOOGLE" -> IapStore.GOOGLE "HORIZON" -> IapStore.HORIZON + "AMAZON" -> IapStore.AMAZON else -> IapStore.UNKNOWN } } diff --git a/libraries/react-native-iap/example/android/app/build.gradle b/libraries/react-native-iap/example/android/app/build.gradle index a6b1b95e..c3bdd793 100644 --- a/libraries/react-native-iap/example/android/app/build.gradle +++ b/libraries/react-native-iap/example/android/app/build.gradle @@ -84,11 +84,19 @@ android { targetSdkVersion = rootProject.ext.targetSdkVersion versionCode = 1 versionName = "1.0" + + def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false + def fireOsEnabled = project.findProperty('fireOsEnabled')?.toBoolean() ?: false + if (horizonEnabled && fireOsEnabled) { + throw new GradleException("react-native-iap example: horizonEnabled and fireOsEnabled cannot both be true") + } + def flavor = fireOsEnabled ? 'amazon' : (horizonEnabled ? 'horizon' : 'play') + missingDimensionStrategy "platform", flavor } compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } packagingOptions { diff --git a/libraries/react-native-iap/example/android/gradle.properties b/libraries/react-native-iap/example/android/gradle.properties index d5e2f043..cd514221 100644 --- a/libraries/react-native-iap/example/android/gradle.properties +++ b/libraries/react-native-iap/example/android/gradle.properties @@ -47,3 +47,8 @@ edgeToEdgeEnabled=false # When true, uses Meta's Platform SDK instead of Google Play Billing # Default: false (uses Google Play Billing) # horizonEnabled=true + +# Enable Fire OS support for Amazon distribution +# Default: false (uses Google Play Billing unless horizonEnabled=true) +# Do not enable with horizonEnabled in the same build. +# fireOsEnabled=true diff --git a/libraries/react-native-iap/example/android/settings.gradle b/libraries/react-native-iap/example/android/settings.gradle index e844166f..1bb5d28a 100644 --- a/libraries/react-native-iap/example/android/settings.gradle +++ b/libraries/react-native-iap/example/android/settings.gradle @@ -1,4 +1,17 @@ -pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } +pluginManagement { + includeBuild("../node_modules/@react-native/gradle-plugin") + repositories { + google() + mavenCentral() + gradlePluginPortal() + } + plugins { + id("com.android.library") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.2.0" apply false + id("org.jetbrains.kotlin.plugin.compose") version "2.2.0" apply false + id("com.vanniktech.maven.publish") version "0.34.0" apply false + } +} plugins { id("com.facebook.react.settings") } extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> def command = ['node', '-e', "console.log(require('@react-native-community/cli').bin)"].execute(null, rootDir).text.trim() @@ -7,3 +20,9 @@ extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> rootProject.name = 'dev.hyo.martie' include ':app' includeBuild('../node_modules/@react-native/gradle-plugin') + +def localOpenIapProject = new File(settingsDir, '../../../../packages/google/openiap') +if (localOpenIapProject.exists()) { + include ':openiap' + project(':openiap').projectDir = localOpenIapProject +} diff --git a/libraries/react-native-iap/package.json b/libraries/react-native-iap/package.json index a2d3eea4..bfae429b 100644 --- a/libraries/react-native-iap/package.json +++ b/libraries/react-native-iap/package.json @@ -1,15 +1,16 @@ { "name": "react-native-iap", - "version": "15.3.2", + "version": "15.4.0-rc.1", "description": "React Native In-App Purchases module for iOS and Android using Nitro", "main": "./lib/module/index.js", + "react-native": "./src/index.ts", "types": "./lib/typescript/src/index.d.ts", "exports": { ".": { "source": "./src/index.ts", + "react-native": "./src/index.ts", "types": "./lib/typescript/src/index.d.ts", - "default": "./lib/module/index.js", - "react-native": "./src/index.ts" + "default": "./lib/module/index.js" }, "./plugin": "./app.plugin.js", "./app.plugin.js": "./app.plugin.js", @@ -34,9 +35,12 @@ "!android/gradlew", "!android/gradlew.bat", "!android/local.properties", + "!android/src/androidTest", + "!android/src/test", "!**/__tests__", "!**/__fixtures__", "!**/__mocks__", + "!**/*.tsbuildinfo", "!**/.*" ], "scripts": { @@ -59,7 +63,7 @@ "test": "jest --coverage", "test:library": "jest --coverage", "test:example": "yarn workspace rn-iap-example test --coverage", - "test:plugin": "jest plugin --coverage", + "test:plugin": "jest plugin/__tests__ --runInBand --coverage", "test:all": "yarn test:library && yarn test:example", "test:ci": "jest --maxWorkers=2 --coverage", "test:ci:example": "yarn workspace rn-iap-example test --coverage", @@ -118,10 +122,20 @@ "typescript": "^5.9.2" }, "peerDependencies": { + "@amazon-devices/keplerscript-appstore-iap-lib": "~2.12.13", + "@amazon-devices/package-manager-lib": "~1.0.1767254401", "react": "*", "react-native": "*", "react-native-nitro-modules": "^0.35.0" }, + "peerDependenciesMeta": { + "@amazon-devices/keplerscript-appstore-iap-lib": { + "optional": true + }, + "@amazon-devices/package-manager-lib": { + "optional": true + } + }, "workspaces": [ "example" ], diff --git a/libraries/react-native-iap/plugin/__tests__/withIAP-android.test.ts b/libraries/react-native-iap/plugin/__tests__/withIAP-android.test.ts index 06dd8c67..25ab1415 100644 --- a/libraries/react-native-iap/plugin/__tests__/withIAP-android.test.ts +++ b/libraries/react-native-iap/plugin/__tests__/withIAP-android.test.ts @@ -30,6 +30,16 @@ jest.mock('expo/config-plugins', () => ({ const updated = {...original, contents: result.modResults.contents}; return {...config, modResults: updated}; }, + withGradleProperties: (config: any, action: any) => { + const original = config.modResults; + const cfg = {...config, modResults: original.gradleProperties ?? []}; + const result = action(cfg); + const updated = { + ...original, + gradleProperties: result.modResults, + }; + return {...config, modResults: updated}; + }, withInfoPlist: (config: any, action: any) => { const original = config.modResults; const cfg = {...config, modResults: original.plist ?? {}}; @@ -60,16 +70,19 @@ jest.mock('expo/config-plugins', () => ({ describe('withIAP config plugin (Android)', () => { const originalLog = console.log; const originalWarn = console.warn; + const originalError = console.error; beforeEach(() => { jest.resetModules(); console.log = jest.fn(); console.warn = jest.fn(); + console.error = jest.fn(); }); afterEach(() => { console.log = originalLog; console.warn = originalWarn; + console.error = originalError; }); function makeConfig(gradle: string, manifest?: any) { @@ -77,6 +90,7 @@ describe('withIAP config plugin (Android)', () => { modResults: { contents: gradle, manifest: manifest ?? {manifest: {}}, + gradleProperties: [], plist: {}, entitlements: {}, podfile: '', @@ -170,4 +184,106 @@ describe('withIAP config plugin (Android)', () => { const app = res.modResults.manifest.manifest.application[0]; expect(app['meta-data']).toBeUndefined(); }); + + it('uses Fire OS artifact, flavor, and removes Play Billing permission when Fire OS is enabled', () => { + const initial = [ + 'android {', + ' defaultConfig {', + ' }', + '}', + 'dependencies {', + ` implementation "io.github.hyochan.openiap:openiap-google-horizon:0.0.1"`, + '}', + '', + ].join('\n'); + const manifest = { + manifest: { + 'uses-permission': [ + {$: {'android:name': 'com.android.vending.BILLING'}}, + ], + }, + }; + const config = makeConfig(initial, manifest); + const res: any = plugin(config as any, {modules: {fireOS: true}}); + + expect(res.modResults.contents).toContain( + `io.github.hyochan.openiap:openiap-google-amazon:${OPENIAP_VERSION}`, + ); + expect(res.modResults.contents).toContain( + 'missingDimensionStrategy "platform", "amazon"', + ); + expect(res.modResults.contents).not.toContain( + 'openiap-google-horizon:0.0.1', + ); + expect(res.modResults.manifest.manifest['uses-permission']).toHaveLength(0); + expect(res.modResults.gradleProperties).toEqual( + expect.arrayContaining([ + {type: 'property', key: 'horizonEnabled', value: 'false'}, + {type: 'property', key: 'fireOsEnabled', value: 'true'}, + ]), + ); + }); + + it('prefers Fire OS over Horizon when both Android modules are enabled', () => { + const initial = `android {\n defaultConfig {\n }\n}\n\ndependencies {\n}`; + const config = makeConfig(initial, {manifest: {}}); + const res: any = plugin(config as any, { + modules: {fireOS: true, horizon: true}, + }); + + expect(res.modResults.contents).toContain( + `io.github.hyochan.openiap:openiap-google-amazon:${OPENIAP_VERSION}`, + ); + expect(res.modResults.contents).toContain( + 'missingDimensionStrategy "platform", "amazon"', + ); + expect(res.modResults.gradleProperties).toEqual( + expect.arrayContaining([ + {type: 'property', key: 'horizonEnabled', value: 'false'}, + {type: 'property', key: 'fireOsEnabled', value: 'true'}, + ]), + ); + }); + + it('rejects Vega OS combined with Fire OS during prebuild', () => { + const initial = `android {\n defaultConfig {\n }\n}\n\ndependencies {\n}`; + const config = makeConfig(initial, {manifest: {}}); + expect(() => + plugin(config as any, { + modules: {fireOS: true, vega: true}, + }), + ).toThrow(/modules\.vega cannot be combined/); + }); + + it('replaces stale Amazon artifact and platform strategy when returning to Play', () => { + const initial = [ + 'android {', + ' defaultConfig {', + ' missingDimensionStrategy "platform", "amazon"', + ' }', + '}', + 'dependencies {', + ` implementation "io.github.hyochan.openiap:openiap-google-amazon:0.0.1"`, + '}', + '', + ].join('\n'); + const config = makeConfig(initial, {manifest: {}}); + const res: any = plugin(config as any); + + expect(res.modResults.contents).toContain( + `io.github.hyochan.openiap:openiap-google:${OPENIAP_VERSION}`, + ); + expect(res.modResults.contents).not.toContain( + 'openiap-google-amazon:0.0.1', + ); + expect(res.modResults.contents).toContain( + 'missingDimensionStrategy "platform", "play"', + ); + expect(res.modResults.gradleProperties).toEqual( + expect.arrayContaining([ + {type: 'property', key: 'horizonEnabled', value: 'false'}, + {type: 'property', key: 'fireOsEnabled', value: 'false'}, + ]), + ); + }); }); diff --git a/libraries/react-native-iap/plugin/__tests__/withIAP-ios.test.ts b/libraries/react-native-iap/plugin/__tests__/withIAP-ios.test.ts index 6e6247ab..50f08867 100644 --- a/libraries/react-native-iap/plugin/__tests__/withIAP-ios.test.ts +++ b/libraries/react-native-iap/plugin/__tests__/withIAP-ios.test.ts @@ -17,6 +17,16 @@ jest.mock('expo/config-plugins', () => ({ const updated = {...original, contents: result.modResults.contents}; return {...config, modResults: updated}; }, + withGradleProperties: (config: any, action: any) => { + const original = config.modResults; + const cfg = {...config, modResults: original.gradleProperties ?? []}; + const result = action(cfg); + const updated = { + ...original, + gradleProperties: result.modResults, + }; + return {...config, modResults: updated}; + }, withInfoPlist: (config: any, action: any) => { const original = config.modResults; const cfg = {...config, modResults: original.plist ?? {}}; @@ -70,6 +80,7 @@ describe('withIAP config plugin (iOS)', () => { modResults: { contents: options?.gradle ?? 'dependencies {\n}', manifest: options?.manifest ?? {manifest: {application: [{}]}}, + gradleProperties: [], plist: options?.plist ?? {}, entitlements: options?.entitlements ?? {}, podfile: options?.podfile ?? '', diff --git a/libraries/react-native-iap/plugin/src/withIAP.ts b/libraries/react-native-iap/plugin/src/withIAP.ts index 56c34704..0fe3b322 100644 --- a/libraries/react-native-iap/plugin/src/withIAP.ts +++ b/libraries/react-native-iap/plugin/src/withIAP.ts @@ -3,6 +3,7 @@ import { WarningAggregator, withAndroidManifest, withAppBuildGradle, + withGradleProperties, withPodfile, withEntitlementsPlist, withInfoPlist, @@ -49,9 +50,9 @@ export const modifyProjectBuildGradle = (gradle: string): string => { return gradle; }; -const OPENIAP_COORD = 'io.github.hyochan.openiap:openiap-google'; +const OPENIAP_GROUP = 'io.github.hyochan.openiap'; -function loadOpenIapConfig(): {google: string} { +function loadOpenIapVersion(): string { const versionsPath = resolvePath(__dirname, '../../openiap-versions.json'); try { const raw = readFileSync(versionsPath, 'utf8'); @@ -63,7 +64,7 @@ function loadOpenIapConfig(): {google: string} { 'react-native-iap: "google" version missing or invalid in openiap-versions.json', ); } - return {google: googleVersion}; + return googleVersion; } catch (error) { throw new Error( `react-native-iap: Unable to load openiap-versions.json (${error instanceof Error ? error.message : error})`, @@ -71,21 +72,16 @@ function loadOpenIapConfig(): {google: string} { } } -let cachedOpenIapVersion: string | null = null; -const getOpenIapVersion = (): string => { - if (cachedOpenIapVersion) { - return cachedOpenIapVersion; - } - cachedOpenIapVersion = loadOpenIapConfig().google; - return cachedOpenIapVersion; -}; - -const modifyAppBuildGradle = (gradle: string): string => { +const modifyAppBuildGradle = ( + gradle: string, + isHorizonEnabled?: boolean, + isFireOsEnabled?: boolean, +): string => { let modified = gradle; let openiapVersion: string; try { - openiapVersion = getOpenIapVersion(); + openiapVersion = loadOpenIapVersion(); } catch (error) { WarningAggregator.addWarningAndroid( 'react-native-iap', @@ -107,9 +103,27 @@ const modifyAppBuildGradle = (gradle: string): string => { ) .replace(/\n{3,}/g, '\n\n'); - const openiapDep = ` implementation "${OPENIAP_COORD}:${openiapVersion}"`; + const flavor = isFireOsEnabled + ? 'amazon' + : isHorizonEnabled + ? 'horizon' + : 'play'; + const artifactId = isFireOsEnabled + ? 'openiap-google-amazon' + : isHorizonEnabled + ? 'openiap-google-horizon' + : 'openiap-google'; + const openiapCoord = `${OPENIAP_GROUP}:${artifactId}`; + const openiapDep = ` implementation "${openiapCoord}:${openiapVersion}"`; + + const openiapAnyLine = + /^[ \t]*(implementation|api)[ \t]+\(?["']io\.github\.hyochan\.openiap:openiap-google(?:-(?:horizon|amazon))?:[^"']+["']\)?[ \t]*$/gim; + const hadExistingOpeniap = openiapAnyLine.test(modified); + if (hadExistingOpeniap) { + modified = modified.replace(openiapAnyLine, '').replace(/\n{3,}/g, '\n\n'); + } - if (!modified.includes(OPENIAP_COORD)) { + if (!modified.includes(openiapCoord)) { if (!/dependencies\s*{/.test(modified)) { modified += `\n\ndependencies {\n${openiapDep}\n}\n`; } else { @@ -122,18 +136,65 @@ const modifyAppBuildGradle = (gradle: string): string => { } } + // Remove stale OpenIAP platform strategies even when returning to the default + // Play artifact. Otherwise a previous Fire OS/Horizon prebuild can keep + // selecting the wrong local flavor. + const strategyPattern = + /^[ \t]*missingDimensionStrategy[ \t]*\(?[ \t]*["']platform["'][ \t]*,[ \t]*["'](play|horizon|amazon)["'][ \t]*\)?[ \t]*$/gm; + if (strategyPattern.test(modified)) { + modified = modified.replace(strategyPattern, ''); + } + + const defaultConfigRegex = /defaultConfig\s*{/; + if (defaultConfigRegex.test(modified)) { + const strategyLine = ` missingDimensionStrategy "platform", "${flavor}"`; + if (!/missingDimensionStrategy.*platform/.test(modified)) { + modified = addLineToGradle(modified, defaultConfigRegex, strategyLine); + } + } + return modified; }; -const withIapAndroid: ConfigPlugin<{iapkitApiKey?: string} | undefined> = ( - config, - props, -) => { +const withIapAndroid: ConfigPlugin< + | { + iapkitApiKey?: string; + isHorizonEnabled?: boolean; + isFireOsEnabled?: boolean; + } + | undefined +> = (config, props) => { // Add OpenIAP dependency to app build.gradle config = withAppBuildGradle(config, (config) => { config.modResults.contents = modifyAppBuildGradle( config.modResults.contents, + props?.isHorizonEnabled, + props?.isFireOsEnabled, + ); + return config; + }); + + config = withGradleProperties(config, (config) => { + const horizonValue = props?.isHorizonEnabled ?? false; + const fireOsValue = props?.isFireOsEnabled ?? false; + + config.modResults = config.modResults.filter( + (item) => + item.type !== 'property' || + !['horizonEnabled', 'fireOsEnabled'].includes(item.key), ); + + config.modResults.push({ + type: 'property', + key: 'horizonEnabled', + value: String(horizonValue), + }); + config.modResults.push({ + type: 'property', + key: 'fireOsEnabled', + value: String(fireOsValue), + }); + return config; }); @@ -146,21 +207,27 @@ const withIapAndroid: ConfigPlugin<{iapkitApiKey?: string} | undefined> = ( const permissions = manifest.manifest['uses-permission']; const billingPerm = {$: {'android:name': 'com.android.vending.BILLING'}}; - const alreadyExists = permissions.some( - (p) => p.$['android:name'] === 'com.android.vending.BILLING', - ); - if (!alreadyExists) { - permissions.push(billingPerm); - if (!hasLoggedPluginExecution) { - console.log( - '✅ Added com.android.vending.BILLING to AndroidManifest.xml', - ); - } + if (props?.isFireOsEnabled) { + manifest.manifest['uses-permission'] = permissions.filter( + (p) => p.$['android:name'] !== 'com.android.vending.BILLING', + ); } else { - if (!hasLoggedPluginExecution) { - console.log( - 'ℹ️ com.android.vending.BILLING already exists in AndroidManifest.xml', - ); + const alreadyExists = permissions.some( + (p) => p.$['android:name'] === 'com.android.vending.BILLING', + ); + if (!alreadyExists) { + permissions.push(billingPerm); + if (!hasLoggedPluginExecution) { + console.log( + '✅ Added com.android.vending.BILLING to AndroidManifest.xml', + ); + } + } else { + if (!hasLoggedPluginExecution) { + console.log( + 'ℹ️ com.android.vending.BILLING already exists in AndroidManifest.xml', + ); + } } } @@ -366,6 +433,27 @@ type IapPluginProps = { * Get your project key from https://kit.openiap.dev */ iapkitApiKey?: string; + /** + * Optional Android store modules. + * Fire OS takes precedence when both Fire OS and Horizon are enabled. + */ + modules?: { + /** + * Horizon module for Meta Quest/VR devices. + * @platform android + */ + horizon?: boolean; + /** + * Fire OS module for Amazon-distributed Android builds. + * @platform android + */ + fireOS?: boolean; + /** + * Vega OS runtime target. This is not an Android flavor and cannot be + * combined with fireOS or horizon. + */ + vega?: boolean; + }; }; const withIapIosFollyWorkaround: ConfigPlugin = ( @@ -451,8 +539,24 @@ const withIapkitApiKeyIOS: ConfigPlugin = ( }; const withIAP: ConfigPlugin = (config, props) => { + const isFireOsEnabled = props?.modules?.fireOS ?? false; + const isVegaEnabled = props?.modules?.vega ?? false; + if (isVegaEnabled && (isFireOsEnabled || props?.modules?.horizon)) { + throw new Error( + 'react-native-iap: modules.vega cannot be combined with Fire OS or Horizon Android flavors. Vega OS is selected by the kepler runtime, not Gradle.', + ); + } + try { - let result = withIapAndroid(config, {iapkitApiKey: props?.iapkitApiKey}); + const isHorizonEnabled = isFireOsEnabled + ? false + : (props?.modules?.horizon ?? false); + + let result = withIapAndroid(config, { + iapkitApiKey: props?.iapkitApiKey, + isHorizonEnabled, + isFireOsEnabled, + }); result = withIapIosFollyWorkaround(result, props); // Add iOS alternative billing configuration if provided if (props?.iosAlternativeBilling) { diff --git a/libraries/react-native-iap/src/__tests__/index.test.ts b/libraries/react-native-iap/src/__tests__/index.test.ts index 580e79b6..139f4911 100644 --- a/libraries/react-native-iap/src/__tests__/index.test.ts +++ b/libraries/react-native-iap/src/__tests__/index.test.ts @@ -100,6 +100,8 @@ describe('Public API (src/index.ts)', () => { (Platform as any).OS = 'ios'; // Re-require module to ensure fresh state if needed jest.resetModules(); + jest.dontMock('react-native'); + jest.dontMock('../vega'); // Reinstall the NitroModules mock after reset jest.doMock('react-native-nitro-modules', () => ({ NitroModules: { @@ -928,6 +930,50 @@ describe('Public API (src/index.ts)', () => { expect(res.map((p: any) => p.productId).sort()).toEqual(['p1', 's1']); }); + it('Vega path queries purchase updates once', async () => { + jest.resetModules(); + jest.doMock('react-native', () => ({ + Platform: {OS: 'kepler'}, + })); + jest.doMock('react-native-nitro-modules', () => ({ + NitroModules: { + createHybridObject: jest.fn(() => mockIap), + }, + })); + jest.doMock('../vega', () => ({ + getVegaIapModule: jest.fn(() => mockIap), + isVegaOS: jest.fn(() => true), + })); + // eslint-disable-next-line @typescript-eslint/no-var-requires + IAP = require('../index'); + + const nitro = { + id: 't-vega', + productId: 'premium_monthly', + transactionDate: Date.now(), + platform: 'android', + quantity: 1, + purchaseState: 'purchased', + isAutoRenewing: true, + }; + mockIap.getAvailablePurchases.mockResolvedValueOnce([nitro]); + + const res = await IAP.getAvailablePurchases({ + includeSuspendedAndroid: true, + }); + + expect(mockIap.getAvailablePurchases).toHaveBeenCalledTimes(1); + expect(mockIap.getAvailablePurchases).toHaveBeenCalledWith({ + android: {includeSuspended: true}, + }); + expect(res).toEqual([ + expect.objectContaining({ + productId: 'premium_monthly', + platform: 'android', + }), + ]); + }); + it('throws on unsupported platform', async () => { (Platform as any).OS = 'web'; await expect(IAP.getAvailablePurchases()).rejects.toThrow( @@ -1699,6 +1745,44 @@ describe('Public API (src/index.ts)', () => { expect(result.iapkit?.store).toBe('google'); }); + it('should pass Amazon IAPKit payloads through on Android', async () => { + (Platform as any).OS = 'android'; + const mockResult = { + provider: 'iapkit', + iapkit: { + isValid: true, + state: 'ready-to-consume', + store: 'amazon', + }, + }; + mockIap.verifyPurchaseWithProvider.mockResolvedValueOnce(mockResult); + + const result = await IAP.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + apiKey: 'test-api-key', + amazon: { + userId: 'amazon-user', + receiptId: 'amazon-receipt', + sandbox: true, + }, + }, + }); + + expect(mockIap.verifyPurchaseWithProvider).toHaveBeenCalledWith({ + provider: 'iapkit', + iapkit: { + apiKey: 'test-api-key', + amazon: { + userId: 'amazon-user', + receiptId: 'amazon-receipt', + sandbox: true, + }, + }, + }); + expect(result.iapkit?.store).toBe('amazon'); + }); + it('should throw error when provider is not iapkit', async () => { (Platform as any).OS = 'ios'; const mockResult = { diff --git a/libraries/react-native-iap/src/__tests__/utils/type-bridge.test.ts b/libraries/react-native-iap/src/__tests__/utils/type-bridge.test.ts index 624629d2..b569acfb 100644 --- a/libraries/react-native-iap/src/__tests__/utils/type-bridge.test.ts +++ b/libraries/react-native-iap/src/__tests__/utils/type-bridge.test.ts @@ -371,6 +371,23 @@ describe('type-bridge utilities', () => { expect(result.purchaseState).toBe('purchased'); expect(result.autoRenewingAndroid).toBe(true); }); + + it('preserves Amazon store on Android purchases', () => { + const nitroPurchase: NitroPurchase = { + id: 'receipt-amazon', + productId: 'sku-amazon', + transactionDate: 789, + purchaseTokenAndroid: 'receipt-amazon', + platform: 'android', + store: 'amazon', + quantity: 1, + purchaseState: 'purchased', + isAutoRenewing: false, + } as NitroPurchase; + + const result = convertNitroPurchaseToPurchase(nitroPurchase); + expect(result.store).toBe('amazon'); + }); }); describe('validation helpers', () => { @@ -383,12 +400,25 @@ describe('type-bridge utilities', () => { platform: 'ios', } as NitroProduct); - const invalid = validateNitroProduct({ - title: 'missing fields', - } as NitroProduct); - - expect(valid).toBe(true); - expect(invalid).toBe(false); + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => undefined); + + try { + const invalid = validateNitroProduct({ + title: 'missing fields', + } as NitroProduct); + + expect(valid).toBe(true); + expect(invalid).toBe(false); + expect(consoleErrorSpy).toHaveBeenCalledWith( + '[RN-IAP]', + 'NitroProduct missing required field: id', + {title: 'missing fields'}, + ); + } finally { + consoleErrorSpy.mockRestore(); + } }); it('validates NitroPurchase shape', () => { @@ -399,12 +429,25 @@ describe('type-bridge utilities', () => { platform: 'ios', } as NitroPurchase); - const invalid = validateNitroPurchase({ - productId: 'sku', - } as NitroPurchase); - - expect(valid).toBe(true); - expect(invalid).toBe(false); + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => undefined); + + try { + const invalid = validateNitroPurchase({ + productId: 'sku', + } as NitroPurchase); + + expect(valid).toBe(true); + expect(invalid).toBe(false); + expect(consoleErrorSpy).toHaveBeenCalledWith( + '[RN-IAP]', + 'NitroPurchase missing required field: id', + {productId: 'sku'}, + ); + } finally { + consoleErrorSpy.mockRestore(); + } }); }); diff --git a/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts b/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts new file mode 100644 index 00000000..5c359e30 --- /dev/null +++ b/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts @@ -0,0 +1,509 @@ +import {createVegaIapModule, type VegaPurchasingService} from '../vega-adapter'; +import {ErrorCode} from '../types'; + +const createService = (): jest.Mocked => + ({ + getUserData: jest.fn(async () => ({ + responseCode: 1, + userData: { + countryCode: 'US', + marketplace: 'US', + userId: 'amazon-user', + }, + })), + getProductData: jest.fn(async () => ({ + responseCode: 1, + productData: new Map([ + [ + 'coins_100', + { + sku: 'coins_100', + title: '100 Coins', + description: 'Coin pack', + productType: 1, + price: { + priceCurrencyCode: 'USD', + priceStr: '$0.99', + valueInMicros: 990000, + }, + }, + ], + [ + 'premium_monthly', + { + sku: 'premium_monthly', + title: 'Premium Monthly', + description: 'Monthly plan', + productType: 3, + subscriptionPeriod: 'P1M', + price: { + priceCurrencyCode: 'USD', + priceStr: '$4.99', + valueInMicros: 4990000, + }, + }, + ], + ]), + })), + purchase: jest.fn(async () => ({ + responseCode: 0, + receipt: { + receiptId: 'receipt-1', + sku: 'coins_100', + productType: 1, + purchaseDate: new Date('2026-05-11T00:00:00.000Z'), + }, + })), + getPurchaseUpdates: jest.fn(async () => ({ + responseCode: 1, + receiptList: [ + { + receiptId: 'sub-receipt', + sku: 'premium_monthly', + productType: 3, + purchaseDate: new Date('2026-05-10T00:00:00.000Z'), + }, + ], + })), + notifyFulfillment: jest.fn(async () => ({ + responseCode: 1, + })), + }) as unknown as jest.Mocked; + +describe('Amazon Vega adapter', () => { + it('maps Vega products to Nitro Android products', async () => { + const service = createService(); + const module = createVegaIapModule(service); + + const products = await module.fetchProducts( + ['coins_100', 'premium_monthly'], + 'all', + ); + + expect(service.getProductData).toHaveBeenCalledWith({ + skus: ['coins_100', 'premium_monthly'], + }); + expect(products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 'coins_100', + type: 'in-app', + platform: 'android', + }), + expect.objectContaining({ + id: 'premium_monthly', + type: 'subs', + platform: 'android', + subscriptionOfferDetailsAndroid: expect.any(String), + }), + ]), + ); + }); + + it('accepts Amazon Vega string success response codes', async () => { + const service = createService(); + service.getProductData.mockResolvedValueOnce({ + responseCode: 'SUCCESSFUL', + productData: { + coins_100: { + sku: 'coins_100', + title: '100 Coins', + description: 'Coin pack', + productType: 1, + price: { + priceCurrencyCode: 'USD', + priceStr: '$0.99', + valueInMicros: '990000', + }, + }, + }, + }); + const module = createVegaIapModule(service); + + await expect(module.fetchProducts(['coins_100'], 'all')).resolves.toEqual([ + expect.objectContaining({ + id: 'coins_100', + price: 0.99, + }), + ]); + }); + + it('emits a purchase update and finishes with notifyFulfillment', async () => { + const service = createService(); + const module = createVegaIapModule(service); + const listener = jest.fn(); + + module.addPurchaseUpdatedListener(listener); + const result = await module.requestPurchase({ + android: {skus: ['coins_100']}, + }); + + expect(result).toEqual([ + expect.objectContaining({ + productId: 'coins_100', + purchaseToken: 'receipt-1', + store: 'amazon', + }), + ]); + expect(listener).toHaveBeenCalledWith( + expect.objectContaining({ + productId: 'coins_100', + purchaseToken: 'receipt-1', + }), + ); + + await expect( + module.finishTransaction({ + android: {purchaseToken: 'receipt-1', isConsumable: true}, + }), + ).resolves.toEqual( + expect.objectContaining({ + responseCode: 0, + purchaseToken: 'receipt-1', + }), + ); + expect(service.notifyFulfillment).toHaveBeenCalledWith({ + fulfillmentResult: 1, + receiptId: 'receipt-1', + }); + }); + + it('maps Amazon invalid SKU purchase failures to OpenIAP errors', async () => { + const service = createService(); + service.purchase.mockResolvedValueOnce({ + responseCode: 2, + receipt: null, + }); + const module = createVegaIapModule(service); + const errorListener = jest.fn(); + module.addPurchaseErrorListener(errorListener); + + await expect( + module.requestPurchase({ + android: {skus: ['missing_sku']}, + }), + ).rejects.toThrow(`"code":"${ErrorCode.SkuNotFound}"`); + expect(errorListener).toHaveBeenCalledWith( + expect.objectContaining({ + code: ErrorCode.SkuNotFound, + responseCode: 2, + }), + ); + }); + + it('returns active subscriptions from purchase updates', async () => { + const service = createService(); + const module = createVegaIapModule(service); + + const subscriptions = await module.getActiveSubscriptions([ + 'premium_monthly', + ]); + + expect(service.getPurchaseUpdates).toHaveBeenCalledWith({reset: true}); + expect(subscriptions).toEqual([ + expect.objectContaining({ + productId: 'premium_monthly', + isActive: true, + purchaseToken: 'sub-receipt', + }), + ]); + }); + + it('uses cached product types when purchase updates omit productType', async () => { + const service = createService(); + service.getPurchaseUpdates.mockResolvedValue({ + responseCode: 1, + receiptList: [ + { + receiptId: 'sub-receipt', + sku: 'premium_monthly', + purchaseDate: new Date('2026-05-10T00:00:00.000Z'), + }, + ], + }); + const module = createVegaIapModule(service); + + await module.fetchProducts(['premium_monthly'], 'subs'); + + await expect( + module.getActiveSubscriptions(['premium_monthly']), + ).resolves.toEqual([ + expect.objectContaining({ + productId: 'premium_monthly', + isActive: true, + purchaseToken: 'sub-receipt', + }), + ]); + }); + + it('hydrates product types when purchase updates omit productType before fetchProducts', async () => { + const service = createService(); + service.getPurchaseUpdates.mockResolvedValue({ + responseCode: 1, + receiptList: [ + { + receiptId: 'sub-receipt', + sku: 'premium_monthly', + purchaseDate: new Date('2026-05-10T00:00:00.000Z'), + }, + ], + }); + const module = createVegaIapModule(service); + + await expect( + module.getActiveSubscriptions(['premium_monthly']), + ).resolves.toEqual([ + expect.objectContaining({ + productId: 'premium_monthly', + isActive: true, + purchaseToken: 'sub-receipt', + }), + ]); + expect(service.getProductData).toHaveBeenCalledWith({ + skus: ['premium_monthly'], + }); + }); + + it('uses subscription request context when purchase receipts omit productType', async () => { + const service = createService(); + service.purchase.mockResolvedValueOnce({ + responseCode: 0, + receipt: { + receiptId: 'sub-purchase', + sku: 'premium_monthly', + }, + }); + const module = createVegaIapModule(service); + + await expect( + module.requestPurchase({ + android: {skus: ['premium_monthly'], subscriptionOffers: []}, + }), + ).resolves.toEqual([ + expect.objectContaining({ + productId: 'premium_monthly', + isAutoRenewing: true, + autoRenewingAndroid: true, + }), + ]); + }); + + it('loads all paginated Amazon purchase updates', async () => { + const service = createService(); + service.getPurchaseUpdates + .mockResolvedValueOnce({ + responseCode: 1, + hasMore: true, + receiptList: [ + { + receiptId: 'receipt-page-1', + sku: 'coins_100', + productType: 1, + }, + ], + }) + .mockResolvedValueOnce({ + responseCode: 1, + hasMore: false, + receiptList: [ + { + receiptId: 'receipt-page-2', + sku: 'premium_monthly', + productType: 3, + }, + ], + }); + const module = createVegaIapModule(service); + + const purchases = await module.getAvailablePurchases(); + + expect(service.getPurchaseUpdates).toHaveBeenNthCalledWith(1, { + reset: true, + }); + expect(service.getPurchaseUpdates).toHaveBeenNthCalledWith(2, { + reset: false, + }); + expect(purchases.map((purchase) => purchase.id)).toEqual([ + 'receipt-page-1', + 'receipt-page-2', + ]); + }); + + it('excludes suspended purchases unless requested', async () => { + const service = createService(); + service.getPurchaseUpdates.mockResolvedValue({ + responseCode: 1, + receiptList: [ + { + receiptId: 'deferred-sub', + sku: 'premium_monthly', + productType: 3, + isDeferred: true, + }, + ], + }); + const module = createVegaIapModule(service); + + await expect( + module.getAvailablePurchases({ + android: {type: 'subs', includeSuspended: false}, + }), + ).resolves.toEqual([]); + + await expect( + module.getAvailablePurchases({ + android: {type: 'subs', includeSuspended: true}, + }), + ).resolves.toEqual([ + expect.objectContaining({ + id: 'deferred-sub', + isAutoRenewing: false, + isSuspendedAndroid: true, + purchaseState: 'pending', + }), + ]); + }); + + it('verifies Vega receipts through IAPKit Amazon payload', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + Response.json({ + isValid: true, + state: 'READY_TO_CONSUME', + store: 'amazon', + }), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createVegaIapModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + apiKey: 'kit-key', + amazon: { + receiptId: 'receipt-vega-1', + sandbox: true, + }, + }, + }), + ).resolves.toEqual({ + provider: 'iapkit', + iapkit: { + isValid: true, + state: 'ready-to-consume', + store: 'amazon', + }, + }); + + expect(service.getUserData).toHaveBeenCalledWith({}); + expect(fetchMock).toHaveBeenCalledWith( + 'https://kit.openiap.dev/v1/purchase/verify', + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ + Authorization: 'Bearer kit-key', + 'Content-Type': 'application/json', + }), + }), + ); + expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({ + store: 'amazon', + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + sandbox: true, + }); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('wraps non-JSON IAPKit failures as receipt errors', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + new Response('bad gateway', {status: 502}), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createVegaIapModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toThrow(`"message":"HTTP 502"`); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('rejects empty successful IAPKit responses as receipt errors', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + new Response('', {status: 200}), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createVegaIapModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toThrow( + `"message":"IAPKit returned non-JSON response (HTTP 200)."`, + ); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('wraps IAPKit network failures as network errors', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn(async () => { + throw new TypeError('network offline'); + }) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createVegaIapModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toThrow(`"code":"network-error"`); + } finally { + globalThis.fetch = originalFetch; + } + }); +}); diff --git a/libraries/react-native-iap/src/index.ts b/libraries/react-native-iap/src/index.ts index 0f65e6fd..2025ff06 100644 --- a/libraries/react-native-iap/src/index.ts +++ b/libraries/react-native-iap/src/index.ts @@ -64,6 +64,7 @@ import { import {RnIapConsole} from './utils/debug'; import {getSuccessFromPurchaseVariant} from './utils/purchase'; import {parseAppTransactionPayload} from './utils'; +import {getVegaIapModule, isVegaOS} from './vega'; // ------------------------------ // Billing Programs API (Android 8.2.0+) @@ -85,6 +86,7 @@ export type { } from './specs/RnIap.nitro'; export * from './types'; export * from './utils/error'; +export * from './vega'; export type ProductTypeInput = 'inapp' | 'in-app' | 'subs'; @@ -167,6 +169,10 @@ let iapRef: RnIap | null = null; */ export const isNitroReady = (): boolean => { if (iapRef) return true; + if (isVegaOS()) { + iapRef = getVegaIapModule(); + return Boolean(iapRef); + } try { iapRef = NitroModules.createHybridObject('RnIap'); return true; @@ -201,10 +207,25 @@ export const isStandardIOS = (): boolean => { return Platform.OS === 'ios' && !isTVOS() && !isMacOS(); }; +const isAndroidStoreRuntime = (): boolean => { + return Platform.OS === 'android' || isVegaOS(); +}; + const IAP = { get instance(): RnIap { if (iapRef) return iapRef; + if (isVegaOS()) { + const vegaModule = getVegaIapModule(); + if (!vegaModule) { + throw new Error( + 'Amazon Vega IAP module is unavailable. Add @amazon-devices/keplerscript-appstore-iap-lib and build with the React Native Vega kepler platform.', + ); + } + iapRef = vegaModule; + return iapRef; + } + // Attempt to create the HybridObject and map common Nitro/JSI readiness errors try { iapRef = NitroModules.createHybridObject('RnIap'); @@ -732,28 +753,28 @@ const subscriptionBillingIssueNativeHandler: NitroSubscriptionBillingIssueListen } }; -function tryAttachSubscriptionBillingIssueNative(): void { - if (subscriptionBillingIssueNativeAttached) return; - try { - IAP.instance.addSubscriptionBillingIssueListener( - subscriptionBillingIssueNativeHandler, - ); - subscriptionBillingIssueNativeAttached = true; - } catch (e) { - const msg = toErrorMessage(e); - if (msg.includes('Nitro runtime not installed')) { - RnIapConsole.warn( - '[subscriptionBillingIssueListener] Nitro not ready yet; will retry on next registration after initConnection()', +export const subscriptionBillingIssueListener = ( + listener: (purchase: Purchase) => void, +): EventSubscription => { + function tryAttachSubscriptionBillingIssueNative(): void { + if (subscriptionBillingIssueNativeAttached) return; + try { + IAP.instance.addSubscriptionBillingIssueListener( + subscriptionBillingIssueNativeHandler, ); - } else { - throw e; + subscriptionBillingIssueNativeAttached = true; + } catch (e) { + const msg = toErrorMessage(e); + if (msg.includes('Nitro runtime not installed')) { + RnIapConsole.warn( + '[subscriptionBillingIssueListener] Nitro not ready yet; will retry on next registration after initConnection()', + ); + } else { + throw e; + } } } -} -export const subscriptionBillingIssueListener = ( - listener: (purchase: Purchase) => void, -): EventSubscription => { subscriptionBillingIssueJsListeners.add(listener); // Retry attachment every call so a listener registered before initConnection() // doesn't stay permanently inert once Nitro is ready. @@ -852,7 +873,7 @@ export const fetchProducts: QueryField<'fetchProducts'> = async (request) => { // item.type === 'subs' case // For Android, check if subscription items have actual offers if ( - Platform.OS === 'android' && + isAndroidStoreRuntime() && item.platform === 'android' && item.type === 'subs' ) { @@ -971,11 +992,26 @@ export const getAvailablePurchases: QueryField< } return validPurchases.map(convertNitroPurchaseToPurchase); - } else if (Platform.OS === 'android') { - // For Android, we need to call twice for inapp and subs + } else if (isAndroidStoreRuntime()) { const includeSuspended = Boolean( options?.includeSuspendedAndroid ?? false, ); + + if (isVegaOS()) { + const nitroPurchases = await IAP.instance.getAvailablePurchases({ + android: {includeSuspended}, + }); + const validPurchases = nitroPurchases.filter(validateNitroPurchase); + if (validPurchases.length !== nitroPurchases.length) { + RnIapConsole.warn( + `[getAvailablePurchases] Some Vega purchases failed validation: ${nitroPurchases.length - validPurchases.length} invalid`, + ); + } + + return validPurchases.map(convertNitroPurchaseToPurchase); + } + + // For Android Play/Horizon/Fire OS, query in-app items and subscriptions separately. const inappNitroPurchases = await IAP.instance.getAvailablePurchases({ android: {type: 'inapp', includeSuspended}, }); @@ -1073,9 +1109,9 @@ export const getStorefrontIOS: QueryField<'getStorefrontIOS'> = async () => { * @see {@link https://openiap.dev/docs/apis/get-storefront} */ export const getStorefront: QueryField<'getStorefront'> = async () => { - if (Platform.OS !== 'ios' && Platform.OS !== 'android') { + if (Platform.OS !== 'ios' && !isAndroidStoreRuntime()) { RnIapConsole.warn( - '[getStorefront] Storefront lookup is only supported on iOS and Android.', + '[getStorefront] Storefront lookup is only supported on iOS, Android, and Vega OS.', ); return ''; } @@ -1674,7 +1710,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( 'Invalid request for iOS. The `sku` property is required.', ); } - } else if (Platform.OS === 'android') { + } else if (isAndroidStoreRuntime()) { // Support both 'google' (recommended) and 'android' (deprecated) fields const androidRequest = perPlatformRequest.google ?? perPlatformRequest.android; @@ -1742,7 +1778,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( // Support both 'google' (recommended) and 'android' (deprecated) fields const androidRequestSource = perPlatformRequest.google ?? perPlatformRequest.android; - if (Platform.OS === 'android' && androidRequestSource) { + if (isAndroidStoreRuntime() && androidRequestSource) { const androidRequest = isSubs ? (androidRequestSource as RequestSubscriptionAndroidProps) : (androidRequestSource as RequestPurchaseAndroidProps); @@ -1849,7 +1885,7 @@ export const finishTransaction: MutationField<'finishTransaction'> = async ( transactionId: purchase.id, }, }; - } else if (Platform.OS === 'android') { + } else if (isAndroidStoreRuntime()) { const token = purchase.purchaseToken ?? undefined; if (!token) { @@ -1907,9 +1943,9 @@ export const acknowledgePurchaseAndroid: MutationField< 'acknowledgePurchaseAndroid' > = async (purchaseToken) => { try { - if (Platform.OS !== 'android') { + if (!isAndroidStoreRuntime()) { throw new Error( - 'acknowledgePurchaseAndroid is only available on Android', + 'acknowledgePurchaseAndroid is only available on Android and Vega OS', ); } @@ -1948,8 +1984,10 @@ export const consumePurchaseAndroid: MutationField< 'consumePurchaseAndroid' > = async (purchaseToken) => { try { - if (Platform.OS !== 'android') { - throw new Error('consumePurchaseAndroid is only available on Android'); + if (!isAndroidStoreRuntime()) { + throw new Error( + 'consumePurchaseAndroid is only available on Android and Vega OS', + ); } const result = await IAP.instance.finishTransaction({ diff --git a/libraries/react-native-iap/src/specs/RnIap.nitro.ts b/libraries/react-native-iap/src/specs/RnIap.nitro.ts index b15fcd22..3a1dfafc 100644 --- a/libraries/react-native-iap/src/specs/RnIap.nitro.ts +++ b/libraries/react-native-iap/src/specs/RnIap.nitro.ts @@ -87,7 +87,7 @@ export type IapkitPurchaseState = // Store identifier for purchase origin // Defined locally for Nitro codegen (not in GQL schema) -export type IapStore = 'unknown' | 'apple' | 'google' | 'horizon'; +export type IapStore = 'unknown' | 'apple' | 'google' | 'horizon' | 'amazon'; // Purchase verification provider selection // Defined locally for Nitro codegen (not in GQL schema) @@ -148,7 +148,8 @@ export interface NitroReceiptValidationHorizonOptions { userId: VerifyPurchaseHorizonOptions['userId']; } -export type NitroPurchaseUpdatedListenerOptions = PurchaseUpdatedListenerOptions; +export type NitroPurchaseUpdatedListenerOptions = + PurchaseUpdatedListenerOptions; export interface NitroReceiptValidationParams { apple?: NitroReceiptValidationAppleOptions | null; @@ -378,8 +379,18 @@ export interface NitroVerifyPurchaseWithIapkitGoogleProps { purchaseToken: string; } +export interface NitroVerifyPurchaseWithIapkitAmazonProps { + /** Amazon Appstore receipt id returned by PurchaseResponse.getReceipt().getReceiptId(). */ + receiptId: string; + /** Use Amazon RVS Cloud Sandbox for App Tester receipts. */ + sandbox?: boolean | null; + /** Amazon Appstore user id returned by PurchaseResponse.getUserData().getUserId(). */ + userId?: string | null; +} + export interface NitroVerifyPurchaseWithIapkitProps { apiKey?: string | null; + amazon?: NitroVerifyPurchaseWithIapkitAmazonProps | null; apple?: NitroVerifyPurchaseWithIapkitAppleProps | null; google?: NitroVerifyPurchaseWithIapkitGoogleProps | null; } diff --git a/libraries/react-native-iap/src/types.ts b/libraries/react-native-iap/src/types.ts index 518f5ffd..2e3ddc63 100644 --- a/libraries/react-native-iap/src/types.ts +++ b/libraries/react-native-iap/src/types.ts @@ -511,7 +511,7 @@ export type IapEvent = 'purchase-updated' | 'purchase-error' | 'promoted-product export type IapPlatform = 'ios' | 'android'; -export type IapStore = 'unknown' | 'apple' | 'google' | 'horizon'; +export type IapStore = 'unknown' | 'apple' | 'google' | 'horizon' | 'amazon'; /** Unified purchase states from IAPKit verification response. */ export type IapkitPurchaseState = 'entitled' | 'pending-acknowledgment' | 'pending' | 'canceled' | 'expired' | 'ready-to-consume' | 'consumed' | 'unknown' | 'inauthentic'; @@ -1585,7 +1585,8 @@ export type RequestPurchaseProps = * * Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. * - apple: Always targets App Store - * - google: Targets Play Store by default, or Horizon when built with horizon flavor + * - google: Targets Play Store by default, Horizon when built with horizon flavor, + * or Fire OS when built with amazon flavor * (determined at build time, not runtime) */ export interface RequestPurchasePropsByPlatforms { @@ -1679,7 +1680,8 @@ export interface RequestSubscriptionIosProps { * * Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. * - apple: Always targets App Store - * - google: Targets Play Store by default, or Horizon when built with horizon flavor + * - google: Targets Play Store by default, Horizon when built with horizon flavor, + * or Fire OS when built with amazon flavor * (determined at build time, not runtime) */ export interface RequestSubscriptionPropsByPlatforms { @@ -1693,6 +1695,15 @@ export interface RequestSubscriptionPropsByPlatforms { ios?: (RequestSubscriptionIosProps | null); } +export interface RequestVerifyPurchaseWithIapkitAmazonProps { + /** Amazon Appstore receipt id returned by PurchaseResponse.getReceipt().getReceiptId(). */ + receiptId: string; + /** Use Amazon RVS Cloud Sandbox for App Tester receipts. */ + sandbox?: (boolean | null); + /** Amazon Appstore user id returned by PurchaseResponse.getUserData().getUserId(). */ + userId?: (string | null); +} + export interface RequestVerifyPurchaseWithIapkitAppleProps { /** The JWS token returned with the purchase response. */ jws: string; @@ -1708,8 +1719,11 @@ export interface RequestVerifyPurchaseWithIapkitGoogleProps { * * - apple: Verifies via App Store (JWS token) * - google: Verifies via Play Store (purchase token) + * - amazon: Verifies via Amazon Appstore RVS (userId + receiptId) */ export interface RequestVerifyPurchaseWithIapkitProps { + /** Amazon Appstore verification parameters. */ + amazon?: (RequestVerifyPurchaseWithIapkitAmazonProps | null); /** API key used for the Authorization header (Bearer {apiKey}). */ apiKey?: (string | null); /** Apple App Store verification parameters. */ diff --git a/libraries/react-native-iap/src/types/amazon-devices-kepler/index.d.ts b/libraries/react-native-iap/src/types/amazon-devices-kepler/index.d.ts new file mode 100644 index 00000000..b443183e --- /dev/null +++ b/libraries/react-native-iap/src/types/amazon-devices-kepler/index.d.ts @@ -0,0 +1,12 @@ +declare module '@amazon-devices/keplerscript-appstore-iap-lib' { + export const PurchasingService: { + getProductData(request: {skus: string[]}): Promise; + getPurchaseUpdates(request: {reset: boolean}): Promise; + getUserData(request: Record): Promise; + notifyFulfillment(request: { + fulfillmentResult: number; + receiptId: string; + }): Promise; + purchase(request: {sku: string}): Promise; + }; +} diff --git a/libraries/react-native-iap/src/utils/type-bridge.ts b/libraries/react-native-iap/src/utils/type-bridge.ts index 60739027..9bd7fa58 100644 --- a/libraries/react-native-iap/src/utils/type-bridge.ts +++ b/libraries/react-native-iap/src/utils/type-bridge.ts @@ -35,6 +35,7 @@ const STORE_UNKNOWN: IapStore = 'unknown'; const STORE_APPLE: IapStore = 'apple'; const STORE_GOOGLE: IapStore = 'google'; const STORE_HORIZON: IapStore = 'horizon'; +const STORE_AMAZON: IapStore = 'amazon'; const PRODUCT_TYPE_SUBS: ProductType = 'subs'; const PRODUCT_TYPE_IN_APP: ProductType = 'in-app'; const PURCHASE_STATE_PENDING: PurchaseState = 'pending'; @@ -67,6 +68,8 @@ function normalizeStore(value?: Nullable): IapStore { return STORE_GOOGLE; case 'horizon': return STORE_HORIZON; + case 'amazon': + return STORE_AMAZON; default: return STORE_UNKNOWN; } diff --git a/libraries/react-native-iap/src/vega-adapter.ts b/libraries/react-native-iap/src/vega-adapter.ts new file mode 100644 index 00000000..7f556d08 --- /dev/null +++ b/libraries/react-native-iap/src/vega-adapter.ts @@ -0,0 +1,849 @@ +import type { + IapkitPurchaseState, + NitroActiveSubscription, + NitroProduct, + NitroPurchase, + NitroPurchaseResult, + NitroVerifyPurchaseWithProviderProps, + NitroVerifyPurchaseWithProviderResult, + RnIap, +} from './specs/RnIap.nitro'; +import {ErrorCode} from './types'; + +type ResponseOperation = + | 'product-data' + | 'purchase' + | 'purchase-updates' + | 'user-data' + | 'notify-fulfillment'; + +interface VegaPrice { + priceCurrencyCode?: string | null; + priceStr?: string | null; + valueInMicros?: bigint | number | string | null; +} + +interface VegaProduct { + description?: string | null; + freeTrialPeriod?: string | null; + price?: VegaPrice | null; + productType?: unknown; + sku?: string | null; + subscriptionPeriod?: string | null; + title?: string | null; +} + +interface VegaReceipt { + cancelDate?: Date | number | string | null; + deferredDate?: Date | number | string | null; + isCancelled?: boolean | null; + isDeferred?: boolean | null; + productType?: unknown; + purchaseDate?: Date | number | string | null; + receiptId?: string | null; + sku?: string | null; + termSku?: string | null; +} + +interface VegaUserData { + countryCode?: string | null; + marketplace?: string | null; + userId?: string | null; +} + +interface VegaResponse { + responseCode?: unknown; +} + +interface VegaProductDataResponse extends VegaResponse { + productData?: Map | Record | null; + unavailableSkus?: string[] | null; +} + +interface VegaPurchaseResponse extends VegaResponse { + receipt?: VegaReceipt | null; + userData?: VegaUserData | null; +} + +interface VegaPurchaseUpdatesResponse extends VegaResponse { + hasMore?: boolean | null; + receiptList?: VegaReceipt[] | null; + userData?: VegaUserData | null; +} + +interface VegaUserDataResponse extends VegaResponse { + userData?: VegaUserData | null; +} + +export interface VegaPurchasingService { + getProductData(request: {skus: string[]}): Promise; + getPurchaseUpdates(request: { + reset: boolean; + }): Promise; + getUserData(request: Record): Promise; + notifyFulfillment(request: { + fulfillmentResult: number; + receiptId: string; + }): Promise; + purchase(request: {sku: string}): Promise; +} + +const PRODUCT_TYPE_SUBSCRIPTION = 3; +const FULFILLMENT_RESULT_FULFILLED = 1; +const RESPONSE_SUCCESS = 1; +const PURCHASE_RESPONSE_SUCCESS = 0; +const PURCHASE_STATE_PURCHASED = 1; +const PURCHASE_STATE_PENDING = 2; +const IAPKIT_VERIFY_URL = 'https://kit.openiap.dev/v1/purchase/verify'; + +function createVegaError( + code: ErrorCode, + message: string, + responseCode?: unknown, + productId?: string, +): Error { + return new Error( + JSON.stringify({ + code, + message, + responseCode: typeof responseCode === 'number' ? responseCode : undefined, + debugMessage: message, + productId, + platform: 'android', + }), + ); +} + +function parseVegaErrorPayload(error: unknown): Record { + if (!(error instanceof Error)) return {}; + try { + const parsed = JSON.parse(error.message); + return parsed && typeof parsed === 'object' + ? (parsed as Record) + : {}; + } catch { + return {}; + } +} + +function toPurchaseErrorResult( + error: unknown, + fallbackMessage: string, +): NitroPurchaseResult { + const parsed = parseVegaErrorPayload(error); + const responseCode = parsed.responseCode; + return { + responseCode: typeof responseCode === 'number' ? responseCode : -1, + code: + typeof parsed.code === 'string' ? parsed.code : ErrorCode.PurchaseError, + message: + typeof parsed.message === 'string' + ? parsed.message + : error instanceof Error + ? error.message + : fallbackMessage, + debugMessage: + typeof parsed.debugMessage === 'string' ? parsed.debugMessage : undefined, + purchaseToken: undefined, + }; +} + +function getResponseCode(response?: VegaResponse | null): unknown { + return response?.responseCode; +} + +function responseCodeName(responseCode: unknown): string { + if (typeof responseCode === 'string') { + return responseCode.toUpperCase(); + } + return ''; +} + +function isSuccess( + operation: ResponseOperation, + responseCode: unknown, +): boolean { + if (typeof responseCode === 'number') { + return operation === 'purchase' + ? responseCode === PURCHASE_RESPONSE_SUCCESS + : responseCode === RESPONSE_SUCCESS; + } + + const name = responseCodeName(responseCode); + return name === 'SUCCESSFUL' || name === 'SUCCESS' || name === 'OK'; +} + +function mapErrorCode( + operation: ResponseOperation, + responseCode: unknown, +): ErrorCode { + const name = responseCodeName(responseCode); + if (name.includes('ALREADY_PURCHASED')) return ErrorCode.AlreadyOwned; + if (name.includes('INVALID_SKU')) return ErrorCode.SkuNotFound; + if (name.includes('NOT_SUPPORTED')) return ErrorCode.FeatureNotSupported; + if (name.includes('PENDING')) return ErrorCode.Pending; + if (operation === 'purchase' && name.includes('FAILED')) { + return ErrorCode.UserCancelled; + } + + if (typeof responseCode === 'number') { + if (operation === 'purchase') { + if (responseCode === 1) return ErrorCode.AlreadyOwned; + if (responseCode === 2) return ErrorCode.SkuNotFound; + if (responseCode === 3) return ErrorCode.FeatureNotSupported; + if (responseCode === 4) return ErrorCode.UserCancelled; + } + if (responseCode === 2 && operation !== 'purchase') { + return ErrorCode.FeatureNotSupported; + } + } + + if (operation === 'product-data') return ErrorCode.QueryProduct; + if (operation === 'user-data') return ErrorCode.InitConnection; + return ErrorCode.PurchaseError; +} + +function ensureSuccessful( + operation: ResponseOperation, + response: VegaResponse | null | undefined, + message: string, + productId?: string, +): void { + const responseCode = getResponseCode(response); + if (isSuccess(operation, responseCode)) return; + + throw createVegaError( + mapErrorCode(operation, responseCode), + `${message}. Amazon Vega responseCode=${String(responseCode ?? 'unknown')}`, + responseCode, + productId, + ); +} + +function stringifyJson(value: unknown): string { + return JSON.stringify(value ?? {}); +} + +function toTimestamp(value: unknown): number { + if (value instanceof Date) return value.getTime(); + if (typeof value === 'number' && Number.isFinite(value)) return value; + if (typeof value === 'string') { + const timestamp = Date.parse(value); + return Number.isFinite(timestamp) ? timestamp : Date.now(); + } + return Date.now(); +} + +function toPriceAmountMicros(value: unknown): string { + if (typeof value === 'bigint') return value.toString(); + if (typeof value === 'number' && Number.isFinite(value)) { + return Math.trunc(value).toString(); + } + if (typeof value === 'string' && value.length > 0) return value; + return '0'; +} + +function microsToPrice(value: unknown): number | null { + const micros = + typeof value === 'bigint' ? Number(value) : Number(value ?? Number.NaN); + if (!Number.isFinite(micros)) return null; + return micros / 1_000_000; +} + +function isSubscription(productType: unknown): boolean { + if (typeof productType === 'number') { + return productType === PRODUCT_TYPE_SUBSCRIPTION; + } + + if (typeof productType === 'string') { + return productType.toUpperCase().includes('SUBSCRIPTION'); + } + + return false; +} + +function productTypeToOpenIap(productType: unknown): 'in-app' | 'subs' { + return isSubscription(productType) ? 'subs' : 'in-app'; +} + +function getReceiptSku(receipt: VegaReceipt): string { + return receipt.sku ?? receipt.termSku ?? ''; +} + +function getCachedProductType( + receipt: VegaReceipt, + productTypesBySku: Map, + fallbackSku?: string, +): unknown { + const sku = getReceiptSku(receipt) || fallbackSku || ''; + return sku ? productTypesBySku.get(sku) : undefined; +} + +function productDataToArray( + productData?: Map | Record | null, +): VegaProduct[] { + if (!productData) return []; + if (productData instanceof Map) return Array.from(productData.values()); + return Object.values(productData); +} + +function createPricingPhase(product: VegaProduct) { + const price = product.price ?? {}; + return { + billingCycleCount: 0, + billingPeriod: product.subscriptionPeriod ?? '', + formattedPrice: price.priceStr ?? '', + priceAmountMicros: toPriceAmountMicros(price.valueInMicros), + priceCurrencyCode: price.priceCurrencyCode ?? '', + recurrenceMode: 1, + }; +} + +function createSubscriptionOffer(product: VegaProduct) { + const pricingPhase = createPricingPhase(product); + const sku = product.sku ?? ''; + return { + basePlanId: sku, + offerId: null, + offerTags: [], + offerToken: '', + pricingPhases: { + pricingPhaseList: [pricingPhase], + }, + }; +} + +function createStandardizedSubscriptionOffer(product: VegaProduct) { + const pricingPhase = createPricingPhase(product); + const sku = product.sku ?? ''; + return { + basePlanIdAndroid: sku, + currency: product.price?.priceCurrencyCode ?? '', + displayPrice: product.price?.priceStr ?? '', + id: sku, + offerTagsAndroid: [], + offerTokenAndroid: '', + paymentMode: 'pay-as-you-go', + period: null, + price: microsToPrice(product.price?.valueInMicros) ?? 0, + pricingPhasesAndroid: { + pricingPhaseList: [pricingPhase], + }, + type: 'introductory', + }; +} + +function mapProduct(product: VegaProduct): NitroProduct { + const sku = product.sku ?? ''; + const type = productTypeToOpenIap(product.productType); + const subscriptionOfferDetails = + type === 'subs' ? [createSubscriptionOffer(product)] : null; + const subscriptionOffers = + type === 'subs' ? [createStandardizedSubscriptionOffer(product)] : null; + + return { + id: sku, + title: product.title ?? sku, + description: product.description ?? '', + type, + displayName: product.title ?? sku, + displayPrice: product.price?.priceStr ?? '', + currency: product.price?.priceCurrencyCode ?? '', + price: microsToPrice(product.price?.valueInMicros), + platform: 'android', + introductoryPricePaymentModeIOS: 'empty', + nameAndroid: product.title ?? sku, + subscriptionPeriodAndroid: product.subscriptionPeriod ?? null, + freeTrialPeriodAndroid: product.freeTrialPeriod ?? null, + subscriptionOfferDetailsAndroid: subscriptionOfferDetails + ? stringifyJson(subscriptionOfferDetails) + : null, + subscriptionOffers: subscriptionOffers + ? stringifyJson(subscriptionOffers) + : null, + productStatusAndroid: 'ok', + }; +} + +function mapReceipt( + receipt: VegaReceipt, + fallbackProductType?: unknown, +): NitroPurchase { + const receiptId = receipt.receiptId ?? ''; + const productId = getReceiptSku(receipt); + const type = productTypeToOpenIap(receipt.productType ?? fallbackProductType); + const isPending = Boolean(receipt.isDeferred); + const isCanceled = Boolean(receipt.isCancelled || receipt.cancelDate); + const isActive = !isCanceled && !isPending; + + return { + id: receiptId, + productId, + transactionDate: toTimestamp(receipt.purchaseDate), + purchaseToken: receiptId, + platform: 'android', + store: 'amazon', + quantity: 1, + purchaseState: isPending ? 'pending' : isActive ? 'purchased' : 'unknown', + isAutoRenewing: type === 'subs' && isActive, + purchaseTokenAndroid: receiptId, + dataAndroid: stringifyJson(receipt), + signatureAndroid: null, + autoRenewingAndroid: type === 'subs' && isActive, + purchaseStateAndroid: isPending + ? PURCHASE_STATE_PENDING + : isActive + ? PURCHASE_STATE_PURCHASED + : 0, + isAcknowledgedAndroid: false, + isSuspendedAndroid: Boolean(receipt.isDeferred), + }; +} + +function getSkuFromRequest(request: Parameters[0]) { + const androidRequest = request.google ?? request.android; + const skus = androidRequest?.skus ?? []; + if (skus.length !== 1) { + throw createVegaError( + ErrorCode.DeveloperError, + 'Amazon Vega purchase expects exactly one SKU per request.', + ); + } + return skus[0]!; +} + +export function createVegaIapModule(service: VegaPurchasingService): RnIap { + const productTypesBySku = new Map(); + const purchaseUpdateListeners = new Map< + number, + (purchase: NitroPurchase) => void + >(); + const purchaseErrorListeners = new Set< + (error: NitroPurchaseResult) => void + >(); + let cachedUserData: VegaUserData | null = null; + let nextPurchaseUpdateListenerToken = 1; + + const emitPurchaseUpdated = (purchase: NitroPurchase): void => { + for (const listener of purchaseUpdateListeners.values()) { + listener(purchase); + } + }; + + const emitPurchaseError = (error: NitroPurchaseResult): void => { + for (const listener of purchaseErrorListeners) { + listener(error); + } + }; + + const getStorefront = async (): Promise => { + const response = await service.getUserData({}); + ensureSuccessful('user-data', response, 'Failed to fetch Amazon user data'); + cachedUserData = response.userData ?? null; + return cachedUserData?.marketplace ?? cachedUserData?.countryCode ?? ''; + }; + + const getPurchaseUpdateReceipts = async (): Promise => { + const receipts: VegaReceipt[] = []; + let reset = true; + let hasMore = false; + + do { + const response = await service.getPurchaseUpdates({reset}); + ensureSuccessful( + 'purchase-updates', + response, + 'Failed to fetch Amazon purchase updates', + ); + cachedUserData = response.userData ?? cachedUserData; + receipts.push(...(response.receiptList ?? [])); + hasMore = Boolean(response.hasMore); + reset = false; + } while (hasMore); + + return receipts; + }; + + const hydrateProductTypesForReceipts = async ( + receipts: VegaReceipt[], + ): Promise => { + const missingSkus = new Set(); + + for (const receipt of receipts) { + const sku = getReceiptSku(receipt); + if (!sku) continue; + if (receipt.productType != null) { + productTypesBySku.set(sku, receipt.productType); + } else if (!productTypesBySku.has(sku)) { + missingSkus.add(sku); + } + } + + if (missingSkus.size === 0) return; + + const response = await service.getProductData({ + skus: Array.from(missingSkus), + }); + ensureSuccessful( + 'product-data', + response, + 'Failed to fetch Amazon Vega product data for purchase updates', + ); + + for (const product of productDataToArray(response.productData)) { + if (product.sku) { + productTypesBySku.set(product.sku, product.productType); + } + } + }; + + const getAvailablePurchases = async ( + options?: Parameters[0], + ): Promise => { + const requestedType = options?.android?.type; + const includeSuspended = Boolean(options?.android?.includeSuspended); + const receipts = await getPurchaseUpdateReceipts(); + await hydrateProductTypesForReceipts(receipts); + return receipts + .filter((receipt) => { + if (!includeSuspended && receipt.isDeferred) return false; + const openIapType = productTypeToOpenIap( + receipt.productType ?? + getCachedProductType(receipt, productTypesBySku), + ); + if (requestedType === 'subs') return openIapType === 'subs'; + if (requestedType === 'inapp') return openIapType === 'in-app'; + return true; + }) + .map((receipt) => + mapReceipt(receipt, getCachedProductType(receipt, productTypesBySku)), + ); + }; + + const finishReceipt = async ( + purchaseToken: string, + ): Promise => { + if (!purchaseToken) { + throw createVegaError( + ErrorCode.DeveloperError, + 'purchaseToken is required to finish an Amazon Vega transaction.', + ); + } + + const response = await service.notifyFulfillment({ + fulfillmentResult: FULFILLMENT_RESULT_FULFILLED, + receiptId: purchaseToken, + }); + ensureSuccessful( + 'notify-fulfillment', + response, + 'Failed to notify Amazon Vega fulfillment', + ); + return { + responseCode: 0, + code: '', + message: '', + purchaseToken, + }; + }; + + const verifyWithIapkit = async ( + params: NitroVerifyPurchaseWithProviderProps, + ): Promise => { + function normalizeIapkitState(state: unknown): IapkitPurchaseState { + const normalized = + typeof state === 'string' + ? state.toLowerCase().replaceAll('_', '-') + : 'unknown'; + const states = new Set([ + 'entitled', + 'pending-acknowledgment', + 'pending', + 'canceled', + 'expired', + 'ready-to-consume', + 'consumed', + 'unknown', + 'inauthentic', + ]); + return states.has(normalized as IapkitPurchaseState) + ? (normalized as IapkitPurchaseState) + : 'unknown'; + } + + function extractIapkitErrorMessage(json: unknown): string | null { + if (!json || typeof json !== 'object') return null; + const record = json as Record; + const details = record.details; + if (details && typeof details === 'object') { + const originalError = (details as Record) + .originalError; + if (typeof originalError === 'string') { + try { + return ( + extractIapkitErrorMessage(JSON.parse(originalError)) ?? + originalError + ); + } catch { + return originalError; + } + } + } + + const errors = record.errors; + if (Array.isArray(errors) && errors.length > 0) { + return extractIapkitErrorMessage(errors[0]); + } + + return typeof record.message === 'string' + ? record.message + : typeof record.error === 'string' + ? record.error + : null; + } + + function parseIapkitJsonResponse(text: string): unknown | null { + if (!text.trim()) return null; + try { + return JSON.parse(text); + } catch { + return null; + } + } + + if (params.provider !== 'iapkit') { + throw createVegaError( + ErrorCode.FeatureNotSupported, + `Unsupported purchase verification provider: ${params.provider}.`, + ); + } + + const iapkit = params.iapkit; + const amazon = iapkit?.amazon; + if (!amazon) { + throw createVegaError( + ErrorCode.DeveloperError, + 'Amazon Vega IAPKit verification requires amazon parameters.', + ); + } + + const receiptId = amazon.receiptId.trim(); + if (!receiptId) { + throw createVegaError( + ErrorCode.DeveloperError, + 'Amazon Vega IAPKit verification requires amazon.receiptId.', + ); + } + + let userId = amazon.userId?.trim() ?? ''; + if (!userId) { + const response = await service.getUserData({}); + ensureSuccessful( + 'user-data', + response, + 'Failed to fetch Amazon user data for IAPKit verification', + ); + cachedUserData = response.userData ?? cachedUserData; + userId = cachedUserData?.userId?.trim() ?? ''; + } + if (!userId) { + throw createVegaError( + ErrorCode.DeveloperError, + 'Amazon Vega IAPKit verification could not resolve userId.', + ); + } + + let response: Response; + try { + response = await fetch(IAPKIT_VERIFY_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(iapkit?.apiKey + ? {Authorization: `Bearer ${iapkit.apiKey}`} + : {}), + }, + body: JSON.stringify({ + store: 'amazon', + userId, + receiptId, + ...(amazon.sandbox == null ? {} : {sandbox: amazon.sandbox}), + }), + }); + } catch (error) { + throw createVegaError( + ErrorCode.NetworkError, + error instanceof Error + ? error.message + : 'Failed to reach IAPKit verification endpoint.', + ); + } + const text = await response.text(); + const json = parseIapkitJsonResponse(text); + + if (!response.ok) { + throw createVegaError( + ErrorCode.ReceiptFailed, + extractIapkitErrorMessage(json) ?? `HTTP ${response.status}`, + ); + } + + if (json === null) { + throw createVegaError( + ErrorCode.ReceiptFailed, + `IAPKit returned non-JSON response (HTTP ${response.status}).`, + ); + } + + const result = json as { + isValid?: unknown; + state?: unknown; + store?: unknown; + }; + return { + provider: 'iapkit', + iapkit: { + isValid: result.isValid === true, + state: normalizeIapkitState(result.state), + store: 'amazon', + }, + }; + }; + + const module: Partial = { + async initConnection(): Promise { + await getStorefront(); + return true; + }, + async endConnection(): Promise { + productTypesBySku.clear(); + cachedUserData = null; + purchaseUpdateListeners.clear(); + purchaseErrorListeners.clear(); + return true; + }, + async fetchProducts(skus: string[], type: string): Promise { + if (!Array.isArray(skus) || skus.length === 0) { + throw createVegaError(ErrorCode.EmptySkuList, 'No SKUs provided'); + } + + const response = await service.getProductData({skus}); + ensureSuccessful( + 'product-data', + response, + 'Failed to fetch Amazon Vega products', + ); + + return productDataToArray(response.productData) + .filter((product) => { + if (product.sku) { + productTypesBySku.set(product.sku, product.productType); + } + const openIapType = productTypeToOpenIap(product.productType); + if (type === 'all') return true; + if (type === 'subs') return openIapType === 'subs'; + return openIapType === 'in-app'; + }) + .map(mapProduct); + }, + async requestPurchase( + request: Parameters[0], + ): Promise>> { + let sku: string | undefined; + try { + sku = getSkuFromRequest(request); + const androidRequest = request.google ?? request.android; + const fallbackProductType = Array.isArray( + androidRequest?.subscriptionOffers, + ) + ? PRODUCT_TYPE_SUBSCRIPTION + : productTypesBySku.get(sku); + if (fallbackProductType != null) { + productTypesBySku.set(sku, fallbackProductType); + } + const response = await service.purchase({sku}); + ensureSuccessful( + 'purchase', + response, + 'Failed to complete Amazon Vega purchase', + sku, + ); + + if (!response.receipt) return []; + + cachedUserData = response.userData ?? cachedUserData; + const purchase = mapReceipt(response.receipt, fallbackProductType); + emitPurchaseUpdated(purchase); + return [purchase]; + } catch (error) { + emitPurchaseError( + toPurchaseErrorResult( + error, + 'Failed to complete Amazon Vega purchase', + ), + ); + throw error; + } + }, + getAvailablePurchases, + async getActiveSubscriptions( + subscriptionIds?: string[], + ): Promise { + const requestedIds = new Set(subscriptionIds ?? []); + const purchases = await getAvailablePurchases({android: {type: 'subs'}}); + return purchases + .filter( + (purchase) => + purchase.isAutoRenewing && + (requestedIds.size === 0 || requestedIds.has(purchase.productId)), + ) + .map((purchase) => ({ + productId: purchase.productId, + isActive: true, + transactionId: purchase.id, + purchaseToken: purchase.purchaseToken ?? null, + transactionDate: purchase.transactionDate, + autoRenewingAndroid: purchase.autoRenewingAndroid ?? true, + purchaseTokenAndroid: purchase.purchaseTokenAndroid ?? null, + })); + }, + async hasActiveSubscriptions(subscriptionIds?: string[]): Promise { + const subscriptions = + await module.getActiveSubscriptions?.(subscriptionIds); + return Boolean(subscriptions?.length); + }, + async finishTransaction( + params: Parameters[0], + ): Promise { + const token = params.android?.purchaseToken; + return finishReceipt(token ?? ''); + }, + addPurchaseUpdatedListener(listener): number { + const token = nextPurchaseUpdateListenerToken++; + purchaseUpdateListeners.set(token, listener); + return token; + }, + addPurchaseErrorListener(listener): void { + purchaseErrorListeners.add(listener); + }, + removePurchaseUpdatedListener(token): void { + purchaseUpdateListeners.delete(token); + }, + removePurchaseErrorListener(listener): void { + purchaseErrorListeners.delete(listener); + }, + addPromotedProductListenerIOS(): void {}, + removePromotedProductListenerIOS(): void {}, + async getStorefront(): Promise { + return getStorefront(); + }, + async getStorefrontIOS(): Promise { + return getStorefront(); + }, + async verifyPurchaseWithProvider(params) { + return verifyWithIapkit(params); + }, + }; + + return module as RnIap; +} diff --git a/libraries/react-native-iap/src/vega.kepler.ts b/libraries/react-native-iap/src/vega.kepler.ts new file mode 100644 index 00000000..eed381fe --- /dev/null +++ b/libraries/react-native-iap/src/vega.kepler.ts @@ -0,0 +1,21 @@ +import {Platform} from 'react-native'; +// eslint-disable-next-line import/no-unresolved +import {PurchasingService} from '@amazon-devices/keplerscript-appstore-iap-lib'; +import type {RnIap} from './specs/RnIap.nitro'; +import {createVegaIapModule, type VegaPurchasingService} from './vega-adapter'; + +let cachedVegaModule: RnIap | null = null; + +export const isVegaOS = (): boolean => { + return String(Platform.OS).toLowerCase() === 'kepler'; +}; + +export const getVegaIapModule = (): RnIap | null => { + if (!isVegaOS()) return null; + if (!cachedVegaModule) { + cachedVegaModule = createVegaIapModule( + PurchasingService as unknown as VegaPurchasingService, + ); + } + return cachedVegaModule; +}; diff --git a/libraries/react-native-iap/src/vega.ts b/libraries/react-native-iap/src/vega.ts new file mode 100644 index 00000000..8f6b974b --- /dev/null +++ b/libraries/react-native-iap/src/vega.ts @@ -0,0 +1,10 @@ +import {Platform} from 'react-native'; +import type {RnIap} from './specs/RnIap.nitro'; + +export const isVegaOS = (): boolean => { + return String(Platform.OS).toLowerCase() === 'kepler'; +}; + +export const getVegaIapModule = (): RnIap | null => { + return null; +}; diff --git a/libraries/react-native-iap/yarn.lock b/libraries/react-native-iap/yarn.lock index c6ca6161..7fa9baa0 100644 --- a/libraries/react-native-iap/yarn.lock +++ b/libraries/react-native-iap/yarn.lock @@ -12309,9 +12309,16 @@ __metadata: react-test-renderer: ^19.1.1 typescript: ^5.9.2 peerDependencies: + "@amazon-devices/keplerscript-appstore-iap-lib": ~2.12.13 + "@amazon-devices/package-manager-lib": ~1.0.1767254401 react: "*" react-native: "*" react-native-nitro-modules: ^0.35.0 + peerDependenciesMeta: + "@amazon-devices/keplerscript-appstore-iap-lib": + optional: true + "@amazon-devices/package-manager-lib": + optional: true languageName: unknown linkType: soft diff --git a/llms-full.txt b/llms-full.txt index 561c961e..cfabebcc 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -3,15 +3,16 @@ > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Quick Reference: https://openiap.dev/llms.txt -> Generated: 2026-05-16T12:59:43.331Z +> Generated: 2026-05-18T04:52:45.925Z ## Table of Contents 1. Installation 2. Core APIs (Connection, Products, Purchase, Subscription) 3. Platform-Specific APIs (iOS, Android) -4. Types Reference -5. Error Codes & Handling -6. Implementation Patterns +4. Store Targets (Play, Horizon, Fire OS, Vega OS) +5. Types Reference +6. Error Codes & Handling +7. Implementation Patterns --- @@ -30,19 +31,22 @@ cd ios && pod install ### Swift (iOS/macOS) ```swift // Swift Package Manager -.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.1.9") +.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.2.1") // CocoaPods -pod 'openiap', '~> 2.1.9' +pod 'openiap', '~> 2.2.1' ``` ### Kotlin (Android) ```kotlin // Gradle (build.gradle.kts) -implementation("io.github.hyochan.openiap:openiap-google:2.1.5") +implementation("io.github.hyochan.openiap:openiap-google:2.2.0") // For Meta Horizon OS -implementation("io.github.hyochan.openiap:openiap-google-horizon:2.1.5") +implementation("io.github.hyochan.openiap:openiap-google-horizon:2.2.0") + +// For Fire OS (Amazon Appstore) +implementation("io.github.hyochan.openiap:openiap-google-amazon:2.2.0") ``` ### Flutter @@ -51,13 +55,13 @@ flutter pub add flutter_inapp_purchase ``` ### Godot -Download `godot-iap-2.2.10.zip` from GitHub Releases, extract it to +Download `godot-iap-2.3.0.zip` from GitHub Releases, extract it to `addons/godot-iap/`, then enable the plugin in Project Settings. ### Kotlin Multiplatform ```kotlin dependencies { - implementation("io.github.hyochan:kmp-iap:2.2.8") + implementation("io.github.hyochan:kmp-iap:2.3.0") } ``` @@ -69,7 +73,7 @@ https://central.sonatype.com/artifact/io.github.hyochan/kmp-iap dotnet add package OpenIap.Maui ``` -Current NuGet package version: 1.0.4 +Current NuGet package version: 1.1.0 Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. @@ -83,6 +87,9 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. `packages/google`. - Public surface: generated OpenIAP types plus `useIAP`, listener helpers, and platform-suffixed iOS/Android APIs. +- Android builds can select Play, Horizon, or Fire OS artifacts. + Vega OS resolves a `kepler` JavaScript adapter before creating the Nitro + HybridObject. - Example app: `libraries/react-native-iap/example`. ### expo-iap @@ -90,6 +97,9 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. - Implementation: Expo Modules wrapper over the same native OpenIAP packages. - Public surface: same hook, listener, query, mutation, and platform API shape as `react-native-iap`, adapted for Expo managed/bare workflows. +- Config plugins can select Horizon or Fire OS Android flavors; + Vega OS follows the Onside-style runtime selector pattern with a JavaScript + adapter. - Example app: `libraries/expo-iap/example`. ### flutter_inapp_purchase @@ -98,6 +108,7 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. iOS and Android method channels. - Public surface: singleton `FlutterInappPurchase.instance`, typed `fetchProducts`, purchase streams, and resolver-style methods. +- Android builds can select Play, Horizon, or Fire OS flavors. ### godot-iap - Package: `godot-iap` for Godot 4.x. @@ -136,6 +147,48 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. --- +## Store Targets + +- Google Play: default Android artifact, `openiap-google`. +- Meta Horizon: Android `horizon` flavor, `openiap-google-horizon`. +- Fire OS: Android `amazon` flavor, + `openiap-google-amazon`; set `fireOsEnabled=true` or + `missingDimensionStrategy("platform", "amazon")`. + Runtime adapters are wired for native Android, `react-native-iap`, + `expo-iap`, and `flutter_inapp_purchase`; Godot, KMP, and MAUI have schema + type parity but still need Android wrapper flavor switches. +- Vega OS: not an Android flavor. Target React Native for Vega / Expo only, + using Amazon's JavaScript IAP API through the runtime-selected `kepler` + adapter at the same runtime integration layer as Onside. In Expo or React + Native config plugin options, `modules.vega=true` is only a + runtime-support guard; it does not select an Android flavor and cannot be + combined with `modules.fireOS` or `modules.horizon`. + +### Fire OS + +Fire OS is an Android target for Amazon Appstore distribution. It uses the +`amazon` Gradle flavor and Amazon Appstore SDK. + +Fire OS maps OpenIAP calls to the Amazon Appstore SDK: + +| OpenIAP API | Amazon Appstore SDK mapping | +|-------------|--------------------------| +| `initConnection()` | Register `PurchasingListener`, request user data | +| `fetchProducts()` | `PurchasingService.getProductData` | +| `requestPurchase()` | `PurchasingService.purchase` | +| `getAvailablePurchases()` | `PurchasingService.getPurchaseUpdates(reset=true)` | +| `finishTransaction()` | `PurchasingService.notifyFulfillment(..., FULFILLED)` | + +### Vega OS Runtime + +Vega OS is not Fire OS and must not set `fireOsEnabled=true`; that flag is +only for Android Fire OS builds. Install +`@amazon-devices/keplerscript-appstore-iap-lib` and let +`react-native-iap` / `expo-iap` select the `kepler` adapter at runtime, +similar to how Onside is selected at the runtime integration layer. + +--- + ## Minimal Usage by Framework ### React Native / Expo diff --git a/llms.txt b/llms.txt index 45cfb2e2..ed6f0981 100644 --- a/llms.txt +++ b/llms.txt @@ -3,7 +3,7 @@ > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Full Reference: https://openiap.dev/llms-full.txt -> Generated: 2026-05-16T12:59:43.331Z +> Generated: 2026-05-18T04:52:45.925Z ## Installation @@ -19,12 +19,14 @@ npm install react-native-iap ### Native ```swift // Swift Package Manager -.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.1.9") +.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.2.1") ``` ```kotlin // Gradle -implementation("io.github.hyochan.openiap:openiap-google:2.1.5") +implementation("io.github.hyochan.openiap:openiap-google:2.2.0") +implementation("io.github.hyochan.openiap:openiap-google-horizon:2.2.0") +implementation("io.github.hyochan.openiap:openiap-google-amazon:2.2.0") ``` ```bash @@ -34,20 +36,20 @@ flutter pub add flutter_inapp_purchase ```gdscript # Godot -# Install godot-iap 2.2.10 to addons/godot-iap and enable the plugin +# Install godot-iap 2.3.0 to addons/godot-iap and enable the plugin ``` ```kotlin // Kotlin Multiplatform -implementation("io.github.hyochan:kmp-iap:2.2.8") +implementation("io.github.hyochan:kmp-iap:2.3.0") ``` -```bash -# .NET MAUI -dotnet add package OpenIap.Maui +```xml + + ``` -Current NuGet package version: 1.0.4 +Current NuGet package version: 1.1.0 ## Framework Libraries @@ -80,7 +82,7 @@ await endConnection(); ```typescript const products = await fetchProducts({ products: [ - { id: 'com.app.premium', type: 'inapp' }, + { id: 'com.app.premium', type: 'in-app' }, { id: 'com.app.monthly', type: 'subs' }, ], }); @@ -95,7 +97,7 @@ await requestPurchase({ apple: { sku: 'com.app.premium' }, google: { skus: ['com.app.premium'] }, }, - type: 'inapp', // 'inapp' | 'subs' + type: 'in-app', // 'in-app' | 'subs' }); ``` @@ -103,7 +105,7 @@ await requestPurchase({ ```typescript // CRITICAL: Must call after verification // Android: purchases auto-refund after 3 days if not acknowledged -await finishTransaction(purchase, isConsumable); +await finishTransaction({ purchase, isConsumable }); ``` ### Get Available Purchases @@ -128,7 +130,7 @@ const purchaseUpdateSubscription = purchaseUpdatedListener(async (purchase) => { // 1. Verify purchase on server // 2. Grant entitlement // 3. Finish transaction - await finishTransaction(purchase); + await finishTransaction({ purchase, isConsumable: false }); }); const purchaseErrorSubscription = purchaseErrorListener((error) => { @@ -152,7 +154,7 @@ interface Product { price: string; // Formatted price string priceAmount: number; // Price as number currency: string; // ISO 4217 currency code - type: 'inapp' | 'subs'; + type: 'in-app' | 'subs'; } ``` diff --git a/openiap-versions.json b/openiap-versions.json index e997205c..0dbc6749 100644 --- a/openiap-versions.json +++ b/openiap-versions.json @@ -1,5 +1,5 @@ { "spec": "2.0.2", - "google": "2.2.1", + "google": "2.3.0-rc.1", "apple": "2.2.1" } diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index 0199cfc7..cecc3c52 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -303,6 +303,7 @@ public enum IapStore: String, Codable, CaseIterable { case apple = "apple" case google = "google" case horizon = "horizon" + case amazon = "amazon" } /// Payment mode for subscription offers. @@ -1847,7 +1848,8 @@ public struct RequestPurchaseProps: Codable { /// /// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. /// - apple: Always targets App Store -/// - google: Targets Play Store by default, or Horizon when built with horizon flavor +/// - google: Targets Play Store by default, Horizon when built with horizon flavor, +/// or Fire OS when built with amazon flavor /// (determined at build time, not runtime) public struct RequestPurchasePropsByPlatforms: Codable { /// @deprecated Use google instead @@ -1975,7 +1977,8 @@ public struct RequestSubscriptionIosProps: Codable { /// /// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. /// - apple: Always targets App Store -/// - google: Targets Play Store by default, or Horizon when built with horizon flavor +/// - google: Targets Play Store by default, Horizon when built with horizon flavor, +/// or Fire OS when built with amazon flavor /// (determined at build time, not runtime) public struct RequestSubscriptionPropsByPlatforms: Codable { /// @deprecated Use google instead @@ -2000,6 +2003,25 @@ public struct RequestSubscriptionPropsByPlatforms: Codable { } } +public struct RequestVerifyPurchaseWithIapkitAmazonProps: Codable { + /// Amazon Appstore receipt id returned by PurchaseResponse.getReceipt().getReceiptId(). + public var receiptId: String + /// Use Amazon RVS Cloud Sandbox for App Tester receipts. + public var sandbox: Bool? + /// Amazon Appstore user id returned by PurchaseResponse.getUserData().getUserId(). + public var userId: String? + + public init( + receiptId: String, + sandbox: Bool? = nil, + userId: String? = nil + ) { + self.receiptId = receiptId + self.sandbox = sandbox + self.userId = userId + } +} + public struct RequestVerifyPurchaseWithIapkitAppleProps: Codable { /// The JWS token returned with the purchase response. public var jws: String @@ -2026,7 +2048,10 @@ public struct RequestVerifyPurchaseWithIapkitGoogleProps: Codable { /// /// - apple: Verifies via App Store (JWS token) /// - google: Verifies via Play Store (purchase token) +/// - amazon: Verifies via Amazon Appstore RVS (userId + receiptId) public struct RequestVerifyPurchaseWithIapkitProps: Codable { + /// Amazon Appstore verification parameters. + public var amazon: RequestVerifyPurchaseWithIapkitAmazonProps? /// API key used for the Authorization header (Bearer {apiKey}). public var apiKey: String? /// Apple App Store verification parameters. @@ -2035,10 +2060,12 @@ public struct RequestVerifyPurchaseWithIapkitProps: Codable { public var google: RequestVerifyPurchaseWithIapkitGoogleProps? public init( + amazon: RequestVerifyPurchaseWithIapkitAmazonProps? = nil, apiKey: String? = nil, apple: RequestVerifyPurchaseWithIapkitAppleProps? = nil, google: RequestVerifyPurchaseWithIapkitGoogleProps? = nil ) { + self.amazon = amazon self.apiKey = apiKey self.apple = apple self.google = google diff --git a/packages/apple/Sources/OpenIapModule.swift b/packages/apple/Sources/OpenIapModule.swift index 12888f10..d7ae2b28 100644 --- a/packages/apple/Sources/OpenIapModule.swift +++ b/packages/apple/Sources/OpenIapModule.swift @@ -715,108 +715,35 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { /// Verify via a managed provider (currently IAPKit; the PurchaseVerificationProvider enum exposes only Iapkit today). /// See: https://openiap.dev/docs/features/validation#verify-purchase-with-provider public func verifyPurchaseWithProvider(_ props: VerifyPurchaseWithProviderProps) async throws -> VerifyPurchaseWithProviderResult { - try await ensureConnection() - guard props.provider == .iapkit else { - throw makePurchaseError(code: .featureNotSupported, message: "Provider \(props.provider.rawValue) is not supported") + struct IapkitApplePayload: Codable { + let store: IapStore + let jws: String } - guard let iapkit = props.iapkit else { - throw makePurchaseError(code: .developerError, message: "Missing IAPKit verification parameters") - } - let result = try await verifyPurchaseWithIapkit(props: iapkit) - return VerifyPurchaseWithProviderResult( - iapkit: result, - provider: props.provider - ) - } - // NOTE: This Apple module intentionally sends only Apple payloads to IAPKit. - // The buildIapkitPayload function has a .google branch for type completeness, - // but it is never invoked from this module. - private func verifyPurchaseWithIapkit(props: RequestVerifyPurchaseWithIapkitProps) async throws -> RequestVerifyPurchaseWithIapkitResult { - // URL is a constant and cannot fail, so force unwrap is safe - let url = URL(string: "https://kit.openiap.dev/v1/purchase/verify")! - - // On Apple, only Apple verification is supported - guard props.apple != nil else { - throw makePurchaseError(code: .developerError, message: "IAPKit verification on Apple requires an apple payload") - } - let store: IapStore = .apple - let body = try buildIapkitPayload(props: props, store: store) - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - if let apiKey = props.apiKey, apiKey.isEmpty == false { - request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") - } - request.httpBody = body - - // Log request details for debugging - OpenIapLog.debug("IAPKit request URL: \(url.absoluteString)") - let requestBody = String(data: body, encoding: .utf8) ?? "<\(body.count) non-UTF8 bytes>" - OpenIapLog.debug("IAPKit request body: \(requestBody), bytes=\(body.count)") - - let (data, response) = try await URLSession.shared.data(for: request) - guard let httpResponse = response as? HTTPURLResponse else { - throw makePurchaseError(code: .networkError, message: "Invalid response") - } - guard (200...299).contains(httpResponse.statusCode) else { - let responseBody = String(data: data, encoding: .utf8) ?? "" - OpenIapLog.warn("verifyPurchaseWithProvider failed (HTTP \(httpResponse.statusCode))") - // Extract concise error message from IAPKit response - var errorMessage = "HTTP \(httpResponse.statusCode)" - if let jsonData = responseBody.data(using: .utf8), - let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] { - errorMessage = extractIapkitErrorMessage(from: json) ?? errorMessage + func extractIapkitErrorMessage(from json: [String: Any]) -> String? { + if let details = json["details"] as? [String: Any], + let originalError = details["originalError"] as? String { + if let data = originalError.data(using: .utf8), + let nested = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { + return extractIapkitErrorMessage(from: nested) ?? originalError + } + return originalError } - throw makePurchaseError(code: .receiptFailed, message: errorMessage) - } - // Log only response metadata; the body can contain receipt details. - OpenIapLog.debug("IAPKit verification response received: bytes=\(data.count)") + if let errors = json["errors"] as? [[String: Any]], let firstError = errors.first { + return extractIapkitErrorMessage(from: firstError) + } - // Parse manually to handle extra fields from IAPKit - // API response format: { "store": "apple", "isValid": true, "state": "PURCHASED" } - guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { - OpenIapLog.warn("Failed to parse IAPKit verification response") - throw makePurchaseError(code: .receiptFailed, message: "Unable to parse verification response") - } + if let message = json["message"] as? String, !message.contains("{\"error\"") { + return message + } - // Check for error response format: { "errors": [{ "code": "...", "message": "..." }] } - if let errors = json["errors"] as? [[String: Any]], let firstError = errors.first { - let errorMessage = firstError["message"] as? String ?? "Unknown error" - let errorCode = firstError["code"] as? String ?? "unknown" - OpenIapLog.warn("IAPKit verification error: \(errorCode) - \(errorMessage)") - throw makePurchaseError(code: .receiptFailed, message: errorMessage) + return json["error"] as? String } - let isValid = (json["isValid"] as? Bool) ?? false - let stateString = json["state"] as? String ?? "UNKNOWN" - // IAPKit API returns UPPER_SNAKE_CASE (e.g., "PURCHASED", "PENDING_ACKNOWLEDGMENT") - // Swift enum expects lower-kebab-case (e.g., "purchased", "pending-acknowledgment") - let normalizedState = stateString.lowercased().replacingOccurrences(of: "_", with: "-") - let parsedState = IapkitPurchaseState(rawValue: normalizedState) ?? .unknown - let storeString = json["store"] as? String - let parsedStore = storeString.flatMap { IapStore(rawValue: $0) } ?? store - OpenIapLog.info("IAPKit verification result: store=\(parsedStore.rawValue), isValid=\(isValid), state=\(parsedState.rawValue)") - return RequestVerifyPurchaseWithIapkitResult(isValid: isValid, state: parsedState, store: parsedStore) - } - - private struct IapkitApplePayload: Codable { - let store: IapStore - let jws: String - } - - private struct IapkitGooglePayload: Codable { - let store: IapStore - let purchaseToken: String - } - - private func buildIapkitPayload(props: RequestVerifyPurchaseWithIapkitProps, store: IapStore) throws -> Data { - let encoder = JSONEncoder() - encoder.outputFormatting = [.withoutEscapingSlashes] - switch store { - case .apple: + func buildIapkitPayload(props: RequestVerifyPurchaseWithIapkitProps) throws -> Data { + let encoder = JSONEncoder() + encoder.outputFormatting = [.withoutEscapingSlashes] guard let apple = props.apple else { throw makePurchaseError(code: .developerError, message: "Apple verification parameters are required") } @@ -824,53 +751,83 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { throw makePurchaseError(code: .developerError, message: "JWS is required") } let payload = IapkitApplePayload( - store: store, + store: .apple, jws: apple.jws ) return try encoder.encode(payload) - case .google, .horizon: - guard let google = props.google else { - throw makePurchaseError(code: .developerError, message: "Google verification parameters are required") + } + + func verifyPurchaseWithIapkit(props: RequestVerifyPurchaseWithIapkitProps) async throws -> RequestVerifyPurchaseWithIapkitResult { + let url = URL(string: "https://kit.openiap.dev/v1/purchase/verify")! + + guard props.apple != nil else { + throw makePurchaseError(code: .developerError, message: "IAPKit verification on Apple requires an apple payload") } - guard google.purchaseToken.isEmpty == false else { - throw makePurchaseError(code: .developerError, message: "purchaseToken is required") + let store: IapStore = .apple + let body = try buildIapkitPayload(props: props) + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + if let apiKey = props.apiKey, apiKey.isEmpty == false { + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") } - let payload = IapkitGooglePayload( - store: store, - purchaseToken: google.purchaseToken - ) - return try encoder.encode(payload) - case .unknown: - throw makePurchaseError(code: .developerError, message: "Unknown store type") - } - } + request.httpBody = body - /// Extract concise error message from IAPKit error response. - /// IAPKit returns nested error structures - we extract the deepest originalError for clarity. - private func extractIapkitErrorMessage(from json: [String: Any]) -> String? { - // Try to get details.originalError first (deepest level) - if let details = json["details"] as? [String: Any], - let originalError = details["originalError"] as? String { - // originalError might be a JSON string, try to parse it - if let data = originalError.data(using: .utf8), - let nested = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { - return extractIapkitErrorMessage(from: nested) ?? originalError + OpenIapLog.debug("IAPKit request URL: \(url.absoluteString)") + OpenIapLog.debug("IAPKit request body bytes=\(body.count)") + + let (data, response) = try await URLSession.shared.data(for: request) + guard let httpResponse = response as? HTTPURLResponse else { + throw makePurchaseError(code: .networkError, message: "Invalid response") + } + guard (200...299).contains(httpResponse.statusCode) else { + let responseBody = String(data: data, encoding: .utf8) ?? "" + OpenIapLog.warn("verifyPurchaseWithProvider failed (HTTP \(httpResponse.statusCode))") + var errorMessage = "HTTP \(httpResponse.statusCode)" + if let jsonData = responseBody.data(using: .utf8), + let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] { + errorMessage = extractIapkitErrorMessage(from: json) ?? errorMessage + } + throw makePurchaseError(code: .receiptFailed, message: errorMessage) } - return originalError - } - // Try errors array format: { "errors": [{ "message": "..." }] } - if let errors = json["errors"] as? [[String: Any]], let firstError = errors.first { - return extractIapkitErrorMessage(from: firstError) - } + OpenIapLog.debug("IAPKit verification response received: bytes=\(data.count)") + + guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { + OpenIapLog.warn("Failed to parse IAPKit verification response") + throw makePurchaseError(code: .receiptFailed, message: "Unable to parse verification response") + } + + if let errors = json["errors"] as? [[String: Any]], let firstError = errors.first { + let errorMessage = firstError["message"] as? String ?? "Unknown error" + let errorCode = firstError["code"] as? String ?? "unknown" + OpenIapLog.warn("IAPKit verification error: \(errorCode) - \(errorMessage)") + throw makePurchaseError(code: .receiptFailed, message: errorMessage) + } - // Try message field, but avoid the verbose nested JSON string - if let message = json["message"] as? String, !message.contains("{\"error\"") { - return message + let isValid = (json["isValid"] as? Bool) ?? false + let stateString = json["state"] as? String ?? "UNKNOWN" + let normalizedState = stateString.lowercased().replacingOccurrences(of: "_", with: "-") + let parsedState = IapkitPurchaseState(rawValue: normalizedState) ?? .unknown + let storeString = json["store"] as? String + let parsedStore = storeString.flatMap { IapStore(rawValue: $0) } ?? store + OpenIapLog.info("IAPKit verification result: store=\(parsedStore.rawValue), isValid=\(isValid), state=\(parsedState.rawValue)") + return RequestVerifyPurchaseWithIapkitResult(isValid: isValid, state: parsedState, store: parsedStore) } - // Fallback to error code - return json["error"] as? String + try await ensureConnection() + guard props.provider == .iapkit else { + throw makePurchaseError(code: .featureNotSupported, message: "Provider \(props.provider.rawValue) is not supported") + } + guard let iapkit = props.iapkit else { + throw makePurchaseError(code: .developerError, message: "Missing IAPKit verification parameters") + } + let result = try await verifyPurchaseWithIapkit(props: iapkit) + return VerifyPurchaseWithProviderResult( + iapkit: result, + provider: props.provider + ) } // MARK: - Store Information diff --git a/packages/docs/openiap-versions.json b/packages/docs/openiap-versions.json index e997205c..0dbc6749 100644 --- a/packages/docs/openiap-versions.json +++ b/packages/docs/openiap-versions.json @@ -1,5 +1,5 @@ { "spec": "2.0.2", - "google": "2.2.1", + "google": "2.3.0-rc.1", "apple": "2.2.1" } diff --git a/packages/docs/public/llms-full.txt b/packages/docs/public/llms-full.txt index 561c961e..cfabebcc 100644 --- a/packages/docs/public/llms-full.txt +++ b/packages/docs/public/llms-full.txt @@ -3,15 +3,16 @@ > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Quick Reference: https://openiap.dev/llms.txt -> Generated: 2026-05-16T12:59:43.331Z +> Generated: 2026-05-18T04:52:45.925Z ## Table of Contents 1. Installation 2. Core APIs (Connection, Products, Purchase, Subscription) 3. Platform-Specific APIs (iOS, Android) -4. Types Reference -5. Error Codes & Handling -6. Implementation Patterns +4. Store Targets (Play, Horizon, Fire OS, Vega OS) +5. Types Reference +6. Error Codes & Handling +7. Implementation Patterns --- @@ -30,19 +31,22 @@ cd ios && pod install ### Swift (iOS/macOS) ```swift // Swift Package Manager -.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.1.9") +.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.2.1") // CocoaPods -pod 'openiap', '~> 2.1.9' +pod 'openiap', '~> 2.2.1' ``` ### Kotlin (Android) ```kotlin // Gradle (build.gradle.kts) -implementation("io.github.hyochan.openiap:openiap-google:2.1.5") +implementation("io.github.hyochan.openiap:openiap-google:2.2.0") // For Meta Horizon OS -implementation("io.github.hyochan.openiap:openiap-google-horizon:2.1.5") +implementation("io.github.hyochan.openiap:openiap-google-horizon:2.2.0") + +// For Fire OS (Amazon Appstore) +implementation("io.github.hyochan.openiap:openiap-google-amazon:2.2.0") ``` ### Flutter @@ -51,13 +55,13 @@ flutter pub add flutter_inapp_purchase ``` ### Godot -Download `godot-iap-2.2.10.zip` from GitHub Releases, extract it to +Download `godot-iap-2.3.0.zip` from GitHub Releases, extract it to `addons/godot-iap/`, then enable the plugin in Project Settings. ### Kotlin Multiplatform ```kotlin dependencies { - implementation("io.github.hyochan:kmp-iap:2.2.8") + implementation("io.github.hyochan:kmp-iap:2.3.0") } ``` @@ -69,7 +73,7 @@ https://central.sonatype.com/artifact/io.github.hyochan/kmp-iap dotnet add package OpenIap.Maui ``` -Current NuGet package version: 1.0.4 +Current NuGet package version: 1.1.0 Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. @@ -83,6 +87,9 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. `packages/google`. - Public surface: generated OpenIAP types plus `useIAP`, listener helpers, and platform-suffixed iOS/Android APIs. +- Android builds can select Play, Horizon, or Fire OS artifacts. + Vega OS resolves a `kepler` JavaScript adapter before creating the Nitro + HybridObject. - Example app: `libraries/react-native-iap/example`. ### expo-iap @@ -90,6 +97,9 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. - Implementation: Expo Modules wrapper over the same native OpenIAP packages. - Public surface: same hook, listener, query, mutation, and platform API shape as `react-native-iap`, adapted for Expo managed/bare workflows. +- Config plugins can select Horizon or Fire OS Android flavors; + Vega OS follows the Onside-style runtime selector pattern with a JavaScript + adapter. - Example app: `libraries/expo-iap/example`. ### flutter_inapp_purchase @@ -98,6 +108,7 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. iOS and Android method channels. - Public surface: singleton `FlutterInappPurchase.instance`, typed `fetchProducts`, purchase streams, and resolver-style methods. +- Android builds can select Play, Horizon, or Fire OS flavors. ### godot-iap - Package: `godot-iap` for Godot 4.x. @@ -136,6 +147,48 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. --- +## Store Targets + +- Google Play: default Android artifact, `openiap-google`. +- Meta Horizon: Android `horizon` flavor, `openiap-google-horizon`. +- Fire OS: Android `amazon` flavor, + `openiap-google-amazon`; set `fireOsEnabled=true` or + `missingDimensionStrategy("platform", "amazon")`. + Runtime adapters are wired for native Android, `react-native-iap`, + `expo-iap`, and `flutter_inapp_purchase`; Godot, KMP, and MAUI have schema + type parity but still need Android wrapper flavor switches. +- Vega OS: not an Android flavor. Target React Native for Vega / Expo only, + using Amazon's JavaScript IAP API through the runtime-selected `kepler` + adapter at the same runtime integration layer as Onside. In Expo or React + Native config plugin options, `modules.vega=true` is only a + runtime-support guard; it does not select an Android flavor and cannot be + combined with `modules.fireOS` or `modules.horizon`. + +### Fire OS + +Fire OS is an Android target for Amazon Appstore distribution. It uses the +`amazon` Gradle flavor and Amazon Appstore SDK. + +Fire OS maps OpenIAP calls to the Amazon Appstore SDK: + +| OpenIAP API | Amazon Appstore SDK mapping | +|-------------|--------------------------| +| `initConnection()` | Register `PurchasingListener`, request user data | +| `fetchProducts()` | `PurchasingService.getProductData` | +| `requestPurchase()` | `PurchasingService.purchase` | +| `getAvailablePurchases()` | `PurchasingService.getPurchaseUpdates(reset=true)` | +| `finishTransaction()` | `PurchasingService.notifyFulfillment(..., FULFILLED)` | + +### Vega OS Runtime + +Vega OS is not Fire OS and must not set `fireOsEnabled=true`; that flag is +only for Android Fire OS builds. Install +`@amazon-devices/keplerscript-appstore-iap-lib` and let +`react-native-iap` / `expo-iap` select the `kepler` adapter at runtime, +similar to how Onside is selected at the runtime integration layer. + +--- + ## Minimal Usage by Framework ### React Native / Expo diff --git a/packages/docs/public/llms.txt b/packages/docs/public/llms.txt index 45cfb2e2..ed6f0981 100644 --- a/packages/docs/public/llms.txt +++ b/packages/docs/public/llms.txt @@ -3,7 +3,7 @@ > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Full Reference: https://openiap.dev/llms-full.txt -> Generated: 2026-05-16T12:59:43.331Z +> Generated: 2026-05-18T04:52:45.925Z ## Installation @@ -19,12 +19,14 @@ npm install react-native-iap ### Native ```swift // Swift Package Manager -.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.1.9") +.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.2.1") ``` ```kotlin // Gradle -implementation("io.github.hyochan.openiap:openiap-google:2.1.5") +implementation("io.github.hyochan.openiap:openiap-google:2.2.0") +implementation("io.github.hyochan.openiap:openiap-google-horizon:2.2.0") +implementation("io.github.hyochan.openiap:openiap-google-amazon:2.2.0") ``` ```bash @@ -34,20 +36,20 @@ flutter pub add flutter_inapp_purchase ```gdscript # Godot -# Install godot-iap 2.2.10 to addons/godot-iap and enable the plugin +# Install godot-iap 2.3.0 to addons/godot-iap and enable the plugin ``` ```kotlin // Kotlin Multiplatform -implementation("io.github.hyochan:kmp-iap:2.2.8") +implementation("io.github.hyochan:kmp-iap:2.3.0") ``` -```bash -# .NET MAUI -dotnet add package OpenIap.Maui +```xml + + ``` -Current NuGet package version: 1.0.4 +Current NuGet package version: 1.1.0 ## Framework Libraries @@ -80,7 +82,7 @@ await endConnection(); ```typescript const products = await fetchProducts({ products: [ - { id: 'com.app.premium', type: 'inapp' }, + { id: 'com.app.premium', type: 'in-app' }, { id: 'com.app.monthly', type: 'subs' }, ], }); @@ -95,7 +97,7 @@ await requestPurchase({ apple: { sku: 'com.app.premium' }, google: { skus: ['com.app.premium'] }, }, - type: 'inapp', // 'inapp' | 'subs' + type: 'in-app', // 'in-app' | 'subs' }); ``` @@ -103,7 +105,7 @@ await requestPurchase({ ```typescript // CRITICAL: Must call after verification // Android: purchases auto-refund after 3 days if not acknowledged -await finishTransaction(purchase, isConsumable); +await finishTransaction({ purchase, isConsumable }); ``` ### Get Available Purchases @@ -128,7 +130,7 @@ const purchaseUpdateSubscription = purchaseUpdatedListener(async (purchase) => { // 1. Verify purchase on server // 2. Grant entitlement // 3. Finish transaction - await finishTransaction(purchase); + await finishTransaction({ purchase, isConsumable: false }); }); const purchaseErrorSubscription = purchaseErrorListener((error) => { @@ -152,7 +154,7 @@ interface Product { price: string; // Formatted price string priceAmount: number; // Price as number currency: string; // ISO 4217 currency code - type: 'inapp' | 'subs'; + type: 'in-app' | 'subs'; } ``` diff --git a/packages/docs/src/generated/version-metadata.json b/packages/docs/src/generated/version-metadata.json index 145c8d35..25de380b 100644 --- a/packages/docs/src/generated/version-metadata.json +++ b/packages/docs/src/generated/version-metadata.json @@ -1,12 +1,12 @@ { "_generatedBy": "scripts/sync-versions.sh", - "expoPackageVersion": "4.3.1", - "reactNativePackageVersion": "15.3.2", - "flutterPackageVersion": "9.3.2", - "godotPackageVersion": "2.3.1", - "kmpPackageVersion": "2.3.1", + "expoPackageVersion": "4.4.0-rc.3", + "reactNativePackageVersion": "15.4.0-rc.1", + "flutterPackageVersion": "9.4.0-rc.1", + "godotPackageVersion": "2.4.0-rc.1", + "kmpPackageVersion": "2.4.0-rc.1", "mauiPackageId": "OpenIap.Maui", - "mauiPackageVersion": "1.1.1", + "mauiPackageVersion": "1.2.0-rc.1", "googleCompileSdk": "35", "googleMinSdk": "23", "googlePlayBillingVersion": "8.3.0", diff --git a/packages/docs/src/pages/docs/android-setup.tsx b/packages/docs/src/pages/docs/android-setup.tsx index c40fb755..7868a80b 100644 --- a/packages/docs/src/pages/docs/android-setup.tsx +++ b/packages/docs/src/pages/docs/android-setup.tsx @@ -29,7 +29,12 @@ function AndroidSetup() { Horizon OS Setup Guide {' '} - for Quest-specific configuration using the same Android SDK. + for Quest-specific configuration using the same Android SDK. For Amazon + Fire OS distribution, see the{' '} + + Fire OS Setup Guide + + .
diff --git a/packages/docs/src/pages/docs/ecosystem.tsx b/packages/docs/src/pages/docs/ecosystem.tsx index 26a80011..74a78882 100644 --- a/packages/docs/src/pages/docs/ecosystem.tsx +++ b/packages/docs/src/pages/docs/ecosystem.tsx @@ -95,9 +95,16 @@ function Ecosystem() { > openiap-google-horizon {' '} - flavor to support Meta HorizonOS. Distributed to third party - libraries for consistent bug fixes and features. Of course, third - party libraries can also support HorizonOS thanks to this. + flavor to support Meta HorizonOS and{' '} + + openiap-google-amazon + {' '} + flavor to support Fire OS. Distributed to third party libraries for + consistent bug fixes and features.
  • { // Works with both Apple and Onside purchases - await finishTransaction(purchase, false); + await finishTransaction({ purchase, isConsumable: false }); }, onPurchaseError: (error) => { console.error(error.message); diff --git a/packages/docs/src/pages/docs/features/offer-code-redemption.tsx b/packages/docs/src/pages/docs/features/offer-code-redemption.tsx index 31025e88..5d1fda21 100644 --- a/packages/docs/src/pages/docs/features/offer-code-redemption.tsx +++ b/packages/docs/src/pages/docs/features/offer-code-redemption.tsx @@ -230,7 +230,7 @@ function RedeemCodeButton() { // Verify and finish the transaction const isValid = await verifyPurchaseOnServer(purchase); if (isValid) { - await finishTransaction(purchase, false); + await finishTransaction({ purchase, isConsumable: false }); console.log('Redemption completed successfully'); } }); diff --git a/packages/docs/src/pages/docs/features/runtime-integrations.tsx b/packages/docs/src/pages/docs/features/runtime-integrations.tsx new file mode 100644 index 00000000..4ba76190 --- /dev/null +++ b/packages/docs/src/pages/docs/features/runtime-integrations.tsx @@ -0,0 +1,84 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../components/AnchorLink'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function RuntimeIntegrations() { + useScrollToHash(); + + return ( +
    + +

    Runtime Integrations

    + +

    + Runtime integrations are selected by the SDK after the app starts, + rather than by choosing a different Android flavor or changing purchase + code in screens. Onside and Vega OS live at this layer. +

    + +
    + + Supported Runtimes + + + + + + + + + + + + + + + + + + + + + +
    RuntimeScopeLibraries
    + + Onside + + iOS alternative marketplace runtime + expo-iap +
    + Vega OS + Amazon Vega JavaScript IAP runtime + react-native-iap, expo-iap +
    +
    + +
    + + Not Android Flavors + +

    + Android store flavors remain in the setup guides: Google Play is the + default, Horizon uses the horizon flavor, and Fire OS + uses the amazon flavor. Vega OS is separate because it + runs through Amazon's JavaScript IAP service in the{' '} + kepler runtime. +

    +

    + For Fire OS Android builds, use{' '} + Fire OS Setup. For Vega OS apps, + use the Vega OS Runtime{' '} + guide. +

    +
    +
    + ); +} + +export default RuntimeIntegrations; diff --git a/packages/docs/src/pages/docs/features/validation.tsx b/packages/docs/src/pages/docs/features/validation.tsx index c07a999c..c9a5a660 100644 --- a/packages/docs/src/pages/docs/features/validation.tsx +++ b/packages/docs/src/pages/docs/features/validation.tsx @@ -105,7 +105,7 @@ const result = await verifyPurchase({ if (result.isValid) { await grantEntitlement(purchase.productId); - await finishTransaction(purchase, false); + await finishTransaction({ purchase, isConsumable: false }); }`} ), swift: ( @@ -297,7 +297,7 @@ const result = await verifyPurchaseWithProvider({ if (result.iapkit?.isValid && result.iapkit?.state === 'entitled') { await grantEntitlement(purchase.productId); - await finishTransaction(purchase, false); + await finishTransaction({ purchase, isConsumable: false }); }`} ), swift: ( @@ -475,7 +475,7 @@ if result.iapkit.is_valid and result.iapkit.state == IapkitPurchaseState.ENTITLE if (result.iapkit?.isValid) { // Verification succeeded - grant access - await finishTransaction(purchase); + await finishTransaction({ purchase, isConsumable: false }); grantAccess(); } else { // Verification returned invalid - actually invalid purchase @@ -488,7 +488,7 @@ if result.iapkit.is_valid and result.iapkit.state == IapkitPurchaseState.ENTITLE console.error('Verification failed:', error); // Fail-open approach: grant access anyway - await finishTransaction(purchase); + await finishTransaction({ purchase, isConsumable: false }); grantAccess(); }`}
  • diff --git a/packages/docs/src/pages/docs/features/vega-os.tsx b/packages/docs/src/pages/docs/features/vega-os.tsx new file mode 100644 index 00000000..947aa01c --- /dev/null +++ b/packages/docs/src/pages/docs/features/vega-os.tsx @@ -0,0 +1,263 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../components/AnchorLink'; +import CodeBlock from '../../../components/CodeBlock'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function VegaOSRuntime() { + useScrollToHash(); + + return ( +
    + +

    Vega OS Runtime

    + +

    + Vega OS is not Fire OS and it is not an Android flavor. OpenIAP supports + Vega OS as a runtime-selected JavaScript adapter in{' '} + react-native-iap and expo-iap, using Amazon's + Vega IAP JavaScript API. +

    +

    + Treat this like the Onside integration layer: the library keeps the + normal native module path for iOS, Android, Fire OS, and Horizon, then + switches to the Vega adapter only when the app is running in the{' '} + kepler runtime. +

    + +
    + + Target Matrix + + + + + + + + + + + + + + + + + + + + + + + + + + +
    TargetLibrarySelection
    Vega OS + react-native-iap, expo-iap + + Runtime adapter selected when Platform.OS is{' '} + kepler +
    Fire OSAndroid, React Native, Expo, Flutter + Android amazon flavor. See{' '} + Fire OS Setup. +
    Flutter, Godot, KMP, MAUI, native AndroidNot Vega targetsNo Vega JavaScript runtime adapter
    +
    + +
    + + Requirements + +
      +
    • React Native for Vega or an Expo-compatible Vega app runtime.
    • +
    • + Amazon Vega IAP package installed in the app: + {`{ + "dependencies": { + "@amazon-devices/keplerscript-appstore-iap-lib": "~2.12.13", + "@amazon-devices/package-manager-lib": "~1.0.1767254401" + } +}`} +
    • +
    • + Vega IAP service declarations in manifest.toml: + {`[wants] +[[wants.service]] +id = "com.amazon.iap.core.service" +[[wants.module]] +id = "/com.amazon.iap.core@IIAPCoreUI" + +[needs] +[[needs.module]] +id = "/com.amazon.kepler.appstore.iap.purchase.core@IAppstoreIAPPurchaseCoreService"`} +
    • +
    +
    + +
    + + Setup + +

    + Do not set fireOsEnabled=true for Vega OS. That Gradle + property selects the Fire OS Android flavor only. +

    +

    + In Expo or React Native config plugin options, set{' '} + modules.vega=true only as a runtime-support guard. It + does not select an Android flavor; it prevents accidental combinations + with modules.fireOS or modules.horizon. +

    + +

    + react-native-iap + + # + +

    +

    + Install react-native-iap normally. The package includes a + Vega resolver that loads vega.kepler.ts in the{' '} + kepler runtime. Non-Vega platforms continue creating the + Nitro RnIap HybridObject. +

    + +

    + expo-iap + + # + +

    +

    + Install expo-iap normally. The module resolver checks for + the Vega runtime before falling back to the existing Onside and native + module paths. +

    +
    + +
    + + Usage + +

    + App code keeps the standard OpenIAP API shape. The runtime adapter + maps the calls to Amazon's Vega IAP service. +

    + {`import { + fetchProducts, + finishTransaction, + initConnection, + requestPurchase, +} from 'react-native-iap'; // or 'expo-iap' + +await initConnection(); + +const products = await fetchProducts({ + skus: ['coins_100'], + type: 'in-app', +}); + +await requestPurchase({ + request: { + google: { + skus: ['coins_100'], + }, + }, + type: 'in-app', +}); + +// In the purchase success path: +await finishTransaction({ purchase, isConsumable: true });`} +
    + +
    + + API Mapping + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    OpenIAP APIVega JavaScript IAP API
    + initConnection() + + PurchasingService.getUserData +
    + fetchProducts() + + PurchasingService.getProductData +
    + requestPurchase() + + PurchasingService.purchase +
    + getAvailablePurchases(),{' '} + restorePurchases() + + PurchasingService.getPurchaseUpdates +
    + finishTransaction() + + PurchasingService.notifyFulfillment +
    +
    + +
    + + Current Limitations + +
      +
    • + Vega OS support is limited to react-native-iap and{' '} + expo-iap. +
    • +
    • + Vega OS uses Amazon's JavaScript IAP API, not the Fire OS Android + Appstore SDK. +
    • +
    • + Server-side Amazon Receipt Verification Service integration is not + included in the client package. +
    • +
    • + The OpenIAP store remains amazon for compatibility, + while runtime selection remains Vega-specific. +
    • +
    +
    +
    + ); +} + +export default VegaOSRuntime; diff --git a/packages/docs/src/pages/docs/fireos-setup.tsx b/packages/docs/src/pages/docs/fireos-setup.tsx new file mode 100644 index 00000000..b4a8ccac --- /dev/null +++ b/packages/docs/src/pages/docs/fireos-setup.tsx @@ -0,0 +1,260 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../components/CodeBlock'; +import SEO from '../../components/SEO'; + +function FireOSSetup() { + return ( +
    + +

    Fire OS Setup Guide

    +

    + Fire OS support targets Android apps distributed through the Amazon + Appstore. OpenIAP supports it through the Android package's{' '} + amazon flavor, backed by the Amazon Appstore SDK. +

    +

    + Vega OS is a separate JavaScript runtime target for{' '} + react-native-iap and expo-iap. It does not use + this Android flavor. See the{' '} + Vega OS Runtime guide for that + setup. +

    + +
    +

    + Platform Boundary + + # + +

    +
      +
    • + Fire OS: Android app target using the Amazon Appstore SDK via the{' '} + amazon flavor. +
    • +
    • + Artifact: openiap-google-amazon for native Android + consumers. +
    • +
    • + Framework flag: fireOsEnabled=true selects the Fire OS + Android flavor in React Native, Expo, and Flutter builds. +
    • +
    • + Vega OS: not an Android flavor. Do not set{' '} + fireOsEnabled=true for Vega OS apps. +
    • +
    +
    + +
    +

    + Fire OS + + # + +

    +

    + Use the Amazon artifact when consuming the native Android package + directly for Fire OS: +

    + {`dependencies { + implementation("io.github.hyochan.openiap:openiap-google-amazon:$version") +}`} +

    + In the monorepo or a local Gradle composite build, select the Amazon + flavor: +

    + {`android { + defaultConfig { + missingDimensionStrategy("platform", "amazon") + } +}`} +

    Fire OS apps also need the usual Amazon Appstore setup:

    +
      +
    • + An Amazon Developer account with an Amazon Appstore app record. +
    • +
    • + In-app items configured in the Amazon Developer Console. OpenIAP + maps Amazon consumables and entitlements to in-app, and + subscriptions to subs. +
    • +
    • + Amazon App Tester installed on a Fire OS or compatible Android test + device for sandbox testing. +
    • +
    • + Amazon Appstore public key copied into the Android app's{' '} + src/main/assets directory. +
    • +
    +
    + +
    +

    + Fire OS Frameworks + + # + +

    +

    + React Native and Expo config plugins write the Fire OS Gradle + selection during prebuild. Flutter apps should wire the same property + into the app module's missingDimensionStrategy. Enable + Fire OS builds in the app's android/gradle.properties: +

    + {`fireOsEnabled=true +# Do not set horizonEnabled=true in the same build.`} +

    + Expo and React Native config plugins can write the same Android + selection during prebuild: +

    + {`// Expo +plugins: [['expo-iap', { modules: { fireOS: true } }]] + +// React Native config plugin +plugins: [['react-native-iap', { modules: { fireOS: true } }]]`} +

    + For Flutter, read the property from{' '} + android/gradle.properties in{' '} + android/app/build.gradle and select the plugin flavor: +

    + {`android { + defaultConfig { + def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false + def fireOsEnabled = project.findProperty('fireOsEnabled')?.toBoolean() ?: false + if (horizonEnabled && fireOsEnabled) { + throw new GradleException("horizonEnabled and fireOsEnabled cannot both be true") + } + def flavor = fireOsEnabled ? 'amazon' : (horizonEnabled ? 'horizon' : 'play') + + missingDimensionStrategy 'platform', flavor + } +}`} +

    + The framework code continues to call the normal cross-platform APIs: +

    + {`await initConnection(); + +const products = await fetchProducts({ + skus: ['coins_100'], + type: 'in-app', +}); + +await requestPurchase({ + request: { + google: { + skus: ['coins_100'], + }, + }, + type: 'in-app', +});`} +
    + +
    +

    + Fire OS Flows + + # + +

    +
      +
    • + + initConnection + {' '} + registers the Amazon IAP listener and requests Amazon user data. +
    • +
    • + + fetchProducts + {' '} + calls PurchasingService.getProductData and maps + consumables, entitlements, and subscriptions into OpenIAP Android + product types. +
    • +
    • + + requestPurchase + {' '} + launches Amazon's purchase flow. Amazon accepts one SKU per purchase + request. +
    • +
    • + + getAvailablePurchases + {' '} + and{' '} + + restorePurchases + {' '} + use PurchasingService.getPurchaseUpdates(reset=true). +
    • +
    • + + finishTransaction + + ,{' '} + + acknowledgePurchaseAndroid + + , and{' '} + + consumePurchaseAndroid + {' '} + call Amazon fulfillment with the receipt ID. +
    • +
    +
    + +
    +

    + Current Limitations + + # + +

    +
      +
    • + Server-side Amazon Receipt Verification Service integration is not + included in the Android client package. +
    • +
    • + Sandbox testing still requires Amazon App Tester setup and Amazon's + sandbox mode, for example{' '} + adb shell setprop debug.amazon.sandboxmode debug. +
    • +
    • + Vega OS is intentionally not included in the Fire OS Android flavor. + Use the Vega OS Runtime{' '} + guide for React Native and Expo Vega apps. +
    • +
    • + Godot, KMP, and MAUI currently receive the shared Amazon store enum + and type documentation, but their Android wrappers do not yet expose + an Amazon flavor switch. +
    • +
    • + Google Play billing programs, alternative billing, and subscription + billing issue events are Play-only and return unsupported or no-op + results on Fire OS. +
    • +
    • + Amazon reports both user-cancelled purchases and some generic + purchase failures with FAILED. OpenIAP maps that status + to a cancellation-style purchase error because Amazon does not + expose a more specific client-side code. +
    • +
    +
    +
    + ); +} + +export default FireOSSetup; diff --git a/packages/docs/src/pages/docs/index.tsx b/packages/docs/src/pages/docs/index.tsx index 8468d987..7815b02b 100644 --- a/packages/docs/src/pages/docs/index.tsx +++ b/packages/docs/src/pages/docs/index.tsx @@ -107,11 +107,14 @@ import SubscriptionBillingIssue from './features/subscription-billing-issue'; import Refund from './features/refund'; import Validation from './features/validation'; import Debugging from './features/debugging'; +import RuntimeIntegrations from './features/runtime-integrations'; import AlternativeMarketplace from './features/alternative-marketplace/index'; import AlternativeMarketplaceOnside from './features/alternative-marketplace/onside'; +import VegaOSRuntime from './features/vega-os'; import IOSSetup from './ios-setup'; import AndroidSetup from './android-setup'; import HorizonSetup from './horizon-setup'; +import FireOSSetup from './fireos-setup'; import SetupIndex from './setup/index'; import ReactNativeSetup from './setup/react-native'; import ExpoSetup from './setup/expo'; @@ -623,7 +626,10 @@ function Docs() { @@ -1195,6 +1205,10 @@ function Docs() { } /> } /> } /> + } + /> } @@ -1203,9 +1217,11 @@ function Docs() { path="features/alternative-marketplace/onside" element={} /> + } /> } /> } /> } /> + } /> } /> } /> } /> diff --git a/packages/docs/src/pages/docs/setup/expo.tsx b/packages/docs/src/pages/docs/setup/expo.tsx index fb6c60b8..f69d49d0 100644 --- a/packages/docs/src/pages/docs/setup/expo.tsx +++ b/packages/docs/src/pages/docs/setup/expo.tsx @@ -133,18 +133,18 @@ function ExpoSetup() {

    - expo-iap uses Google Play Billing Library v - {GOOGLE_PLAY_BILLING.version}, which requires{' '} - Kotlin 2.0+. + expo-iap uses OpenIAP Android artifacts backed by Google Play Billing + Library v{GOOGLE_PLAY_BILLING.version}. Use{' '} + Kotlin 2.2+ for Android builds.

    • - Expo SDK 54+: No configuration needed — Kotlin 2.0+ - is included by default. + Expo SDK 54+: Use the default toolchain when it is + already Kotlin 2.2 compatible, or set the version explicitly below.
    • - Expo SDK 53: Kotlin 2.0+ is included natively, but - if you encounter build issues, explicitly set the Kotlin version: + Expo SDK 53: Explicitly set the Kotlin version when + building Android apps:
    @@ -286,9 +286,11 @@ cd ios && pod install`} "iapkitApiKey": "openiap-kit_", "modules": { "onside": true, - "horizon": true + "horizon": true, + "fireOS": false, + "vega": false }, - "google": { + "android": { "horizonAppId": "YOUR_HORIZON_APP_ID" } } @@ -338,7 +340,29 @@ cd ios && pod install`} - google.horizonAppId + modules.fireOS + + boolean + + Enable the Fire OS Android amazon flavor (see{' '} + Fire OS Setup) + + + + + modules.vega + + runtime + + Declares Vega OS runtime support and enables conflict checks; it + does not select an Android flavor. Install Amazon's Vega IAP + package and follow the{' '} + Vega OS Runtime guide. + + + + + android.horizonAppId string Meta Horizon App ID for Quest/VR devices @@ -383,7 +407,7 @@ function Store() { // 2. Grant entitlement // 3. CRITICAL: Finish the transaction // (Android auto-refunds after 3 days if not called!) - await finishTransaction(purchase, false); // true for consumables + await finishTransaction({ purchase, isConsumable: false }); // true for consumables }, onPurchaseError: (error) => { if (error.code === ErrorCode.UserCancelled) return; @@ -654,9 +678,9 @@ EXPO_TV=1 npx expo run:ios --device "Apple TV 4K (3rd generation)"`} }} > Warning: Expo SDK 52 (React Native 0.76.x) uses - Kotlin 1.9.x, which is incompatible with Google Play Billing Library - v8 (requires Kotlin 2.0+). Upgrading to SDK 53+ is - the recommended solution. + Kotlin 1.9.x, which is incompatible with the current OpenIAP Android + artifacts. Upgrading to SDK 53+ and setting Kotlin + 2.2.0 is the recommended solution.

    diff --git a/packages/docs/src/pages/docs/setup/flutter.tsx b/packages/docs/src/pages/docs/setup/flutter.tsx index 6f4da438..5f3c59d5 100644 --- a/packages/docs/src/pages/docs/setup/flutter.tsx +++ b/packages/docs/src/pages/docs/setup/flutter.tsx @@ -127,8 +127,11 @@ function FlutterSetup() { > Note: The missingDimensionStrategy{' '} configuration is required since v7.1.14 due to product flavor support - for Meta Horizon OS. For Meta Quest support, see the{' '} - Horizon OS Setup Guide. + for Meta Horizon OS and Fire OS. For Meta Quest support, see the{' '} + Horizon OS Setup Guide. For Amazon + Fire OS builds, map fireOsEnabled=true to the{' '} + amazon flavor in your app Gradle file and see the{' '} + Fire OS Setup Guide.

    ProGuard Rules (if using ProGuard)

    @@ -310,6 +313,14 @@ final allPurchases = await iap.getAvailablePurchases( Horizon OS Setup — Meta Quest in-app purchase configuration +
  • + Fire OS Setup — Fire OS Android + flavor configuration +
  • +
  • + Vega OS Runtime — React Native + / Expo only; not a Flutter target +
  • Horizon OS Setup — Meta Quest developer dashboard for the Horizon Store
  • +
  • + Fire OS Setup — Android{' '} + amazon flavor for Amazon Appstore distribution +
  • +
  • + Vega OS Runtime — React + Native / Expo JavaScript adapter for Amazon Vega apps +
  • diff --git a/packages/docs/src/pages/docs/setup/react-native.tsx b/packages/docs/src/pages/docs/setup/react-native.tsx index c547cf34..a44ca4b1 100644 --- a/packages/docs/src/pages/docs/setup/react-native.tsx +++ b/packages/docs/src/pages/docs/setup/react-native.tsx @@ -16,7 +16,8 @@ function ReactNativeSetup() {

    react-native-iap provides in-app purchase support for React Native apps using Nitro Modules. It supports StoreKit 2 on iOS and - Google Play Billing {GOOGLE_PLAY_BILLING.version}+ on Android. + Google Play Billing {GOOGLE_PLAY_BILLING.version}+ on Android by + default, with optional Horizon and Fire OS Android flavors.

    +
  • + For Fire OS builds, set fireOsEnabled=true in{' '} + android/gradle.properties and see the{' '} + Fire OS Setup Guide +
  • +
  • + For Vega OS, do not use an Android flavor. Install Amazon's Vega IAP + package and follow the{' '} + Vega OS Runtime guide. +
  • @@ -228,7 +239,7 @@ function Store() { // 2. Grant entitlement // 3. CRITICAL: Finish the transaction // (Android auto-refunds after 3 days if not called!) - await finishTransaction(purchase, false); // true for consumables + await finishTransaction({ purchase, isConsumable: false }); // true for consumables }, onPurchaseError: (error) => { if (error.code === ErrorCode.UserCancelled) return; @@ -339,7 +350,7 @@ await initConnection(); const purchaseSub = purchaseUpdatedListener(async (purchase) => { // Validate on server, then finish transaction // CRITICAL: Android auto-refunds after 3 days if not called! - await finishTransaction(purchase, false); // true for consumables + await finishTransaction({ purchase, isConsumable: false }); // true for consumables }); const errorSub = purchaseErrorListener((error) => { @@ -425,6 +436,14 @@ switch (error.code) { Horizon OS Setup — Meta Quest in-app purchase configuration +
  • + Fire OS Setup — Fire OS Android + flavor configuration +
  • +
  • + Vega OS Runtime — React Native + for Vega runtime adapter +
  • + + May 23, 2026 — Fire OS support + + +

    + Adds Amazon Fire OS support to OpenIAP. Starting with{' '} + openiap-google 2.3.0, Android builds can target Google + Play, Meta Horizon, or Amazon Appstore from the same native package + family. The OpenIAP Spec remains 2.0.3; this + rollout adds the Fire OS runtime flavor, framework build flags, + Amazon Appstore receipt verification paths, and shared subscription + state mapping. +

    + +
      +
    • + Amazon Fire OS flavor — native Android publishes{' '} + openiap-google-amazon, backed by the Amazon Appstore + SDK and selected with the amazon Gradle flavor. +
    • +
    • + Framework rollout — React Native, Expo, Flutter, + Godot, KMP, and MAUI releases include Fire OS metadata and build + integration alongside existing Play and Horizon support. +
    • +
    • + IAPKit verification — Amazon Appstore receipts + can be verified through IAPKit with userId and{' '} + receiptId, while the shared secret remains on the + server. +
    • +
    • + Shared subscription state — Fire OS + subscriptions flow through the same OpenIAP{' '} + + fetchProducts + + ,{' '} + + requestPurchase + + ,{' '} + + getActiveSubscriptions + + , and{' '} + + getAvailablePurchases + {' '} + lifecycle used by the other stores. The Amazon adapter hydrates + product type and subscription group metadata so app and framework + code do not need store-specific receipt alias handling. +
    • +
    • + Setup guide — see{' '} + Fire OS Setup for Amazon App + Tester, public key, and framework flag details. +
    • +
    + +
    +
  • + ), + }, + // May 19, 2026 — Android Billing callback race hotfix { id: 'android-billing-callback-race-hotfix-2026-05-19', diff --git a/packages/docs/vite.config.ts b/packages/docs/vite.config.ts index 83048a50..b4f05c3e 100644 --- a/packages/docs/vite.config.ts +++ b/packages/docs/vite.config.ts @@ -35,6 +35,27 @@ export default defineConfig({ return 'vendor-lucide'; } } + if (id.includes('/src/pages/docs/updates/')) { + return 'docs-updates'; + } + if (id.includes('/src/pages/docs/apis/')) { + return 'docs-apis'; + } + if (id.includes('/src/pages/docs/types/')) { + return 'docs-types'; + } + if (id.includes('/src/pages/docs/features/')) { + return 'docs-features'; + } + if (id.includes('/src/pages/docs/setup/')) { + return 'docs-setup'; + } + if (id.includes('/src/pages/docs/guides/')) { + return 'docs-guides'; + } + if (id.includes('/src/pages/docs/foundation/')) { + return 'docs-foundation'; + } }, }, }, diff --git a/packages/google/CONTRIBUTING.md b/packages/google/CONTRIBUTING.md index e40c02ba..7caa15f9 100644 --- a/packages/google/CONTRIBUTING.md +++ b/packages/google/CONTRIBUTING.md @@ -33,9 +33,18 @@ cd openiap/packages/google adb shell am start -n dev.hyo.martie/.MainActivity ``` -## Horizon OS Development (Meta Quest) +## Store Flavor Development + +This library supports Google Play Store, Meta Horizon OS (Quest devices), and +Fire OS using product flavors. -This library supports both Google Play Store and Meta Horizon OS (Quest devices) using product flavors. +| Variant | Store | +| ------------------------------------- | ------------------------------ | +| **playDebug** / **playRelease** | Google Play Store billing | +| **horizonDebug** / **horizonRelease** | Meta Horizon OS billing | +| **amazonDebug** / **amazonRelease** | Fire OS billing | + +## Horizon OS Development (Meta Quest) ### Setting Up Horizon App ID @@ -65,11 +74,6 @@ EXAMPLE_HORIZON_APP_ID=your_horizon_app_id_here - Set **openiap** module to **horizonDebug** - Run the app (App ID will be read from `local.properties`) -### Build Variants - -- **playDebug** / **playRelease** - Google Play Store billing -- **horizonDebug** / **horizonRelease** - Meta Horizon OS billing - ### Testing on Quest Devices ```bash @@ -85,6 +89,22 @@ adb logcat | grep -E "OpenIap|Horizon" **Note**: The Horizon App ID is required for Horizon Billing to work. Without it, the billing client will fail to connect. +## Fire OS Development + +The Amazon flavor uses the Amazon Appstore SDK and is intended for Fire OS / +Amazon-distributed Android builds. + +```bash +# Build Fire OS variant +./gradlew :Example:assembleAmazonDebug + +# Install Fire OS variant +./gradlew :Example:installAmazonDebug + +# View logs +adb logcat | grep -E "OpenIap|Amazon" +``` + ## Generated Types - All GraphQL models in `openiap/src/main/java/dev/hyo/openiap/Types.kt` are generated from the [`openiap` monorepo](https://github.com/hyodotdev/openiap/tree/main/packages/gql). When you update API behavior, adjust the upstream type generator first so the Kotlin output stays in sync across platforms. diff --git a/packages/google/CONVENTION.md b/packages/google/CONVENTION.md index 017bfbed..b453a3fb 100644 --- a/packages/google/CONVENTION.md +++ b/packages/google/CONVENTION.md @@ -7,6 +7,7 @@ **IMPORTANT**: Since this is an Android-only package, **DO NOT add `Android` suffix** to function names, even for Android-specific APIs. **✅ Correct**: + ```kotlin fun acknowledgePurchase() fun consumePurchase() @@ -16,6 +17,7 @@ fun isHorizonEnvironment(context: Context) ``` **❌ Incorrect**: + ```kotlin fun acknowledgePurchaseAndroid() // Don't add Android suffix fun consumePurchaseAndroid() // Don't add Android suffix @@ -35,6 +37,7 @@ types that contrast with iOS types), or when it is a generated GraphQL operation/handler identifier that must match the schema. ### Enum Values + - Enum values in this codebase must use **kebab-case** (e.g., `non-consumable`, `in-app`, `user-cancelled`) - This matches the convention used in the auto-generated Types.kt from GraphQL schemas - Do not use snake_case (e.g., `non_consumable`) or camelCase for enum raw values @@ -57,33 +60,36 @@ operation/handler identifier that must match the schema. - `OpenIapStore` and other consumers must call the module through these handler properties rather than direct suspend functions, unpacking any wrapper results (such as `RequestPurchaseResultPurchases`) as needed. - Keep helper wiring inside `OpenIapModule`—avoid reintroducing extension builders like `createQueryHandlers`; the module itself owns `queryHandlers`, `mutationHandlers`, and `subscriptionHandlers` values so wiring stays localized and in sync with the typealiases. -## Build Flavors (Play vs Horizon) +## Build Flavors -This package supports **two build flavors**: +This package supports **three build flavors**: -| Flavor | Store | Source Directory | -|--------|-------|------------------| -| `play` (default) | Google Play Store | `src/play/` | -| `horizon` | Meta Quest Store | `src/horizon/` | +| Flavor | Store | Source Directory | +| ---------------- | ---------------------- | ---------------- | +| `play` (default) | Google Play Store | `src/play/` | +| `horizon` | Meta Quest Store | `src/horizon/` | +| `amazon` | Fire OS | `src/amazon/` | ### Source Directory Structure ```text openiap/src/ -├── main/ # Shared code (used by both flavors) +├── main/ # Shared code (used by every flavor) ├── play/ # Play Store specific implementations -└── horizon/ # Meta Horizon specific implementations +├── horizon/ # Meta Horizon specific implementations +└── amazon/ # Fire OS specific implementations ``` ### When to Use Each Directory -- **`src/main/`**: Code that works for BOTH Play and Horizon +- **`src/main/`**: Code that works for Play, Horizon, and Fire OS - **`src/play/`**: Play Store specific code (Google Play Billing API) - **`src/horizon/`**: Horizon specific code (Meta S2S API) +- **`src/amazon/`**: Fire OS specific code (Amazon Appstore SDK) -### Critical: Test Both Flavors +### Critical: Test All Flavors -When modifying shared code in `src/main/`, **ALWAYS test both flavors**: +When modifying shared code in `src/main/`, **ALWAYS test every flavor**: ```bash # Play flavor @@ -91,21 +97,25 @@ When modifying shared code in `src/main/`, **ALWAYS test both flavors**: # Horizon flavor ./gradlew :openiap:compileHorizonDebugKotlin + +# Fire OS flavor +./gradlew :openiap:compileAmazonDebugKotlin ``` -### Horizon-Specific APIs +### Flavor-Specific Helpers -Some APIs exist only in Horizon flavor: +Some implementation helpers exist only on specific Android flavors: -- `getAvailableItems` - Fetch catalog items (Horizon only) +- `getAvailableItems` - Fetch available purchases for Play, Horizon, and Amazon - `VerifyPurchaseHorizonOptions` - Horizon verification parameters - `VerifyPurchaseResultHorizon` - Horizon verification result ## Regeneration Checklist - Run `./scripts/generate-types.sh` whenever GraphQL schema definitions change. -- After regenerating, run the relevant Gradle targets for **BOTH flavors**: +- After regenerating, run the relevant Gradle targets for every flavor: ```bash ./gradlew :openiap:compilePlayDebugKotlin ./gradlew :openiap:compileHorizonDebugKotlin + ./gradlew :openiap:compileAmazonDebugKotlin ``` diff --git a/packages/google/Example/build.gradle.kts b/packages/google/Example/build.gradle.kts index 58964f7f..07c57969 100644 --- a/packages/google/Example/build.gradle.kts +++ b/packages/google/Example/build.gradle.kts @@ -96,6 +96,12 @@ android { ?: "" manifestPlaceholders["OCULUS_APP_ID"] = appId } + + // Amazon flavor - Amazon Appstore SDK IAP + create("amazon") { + dimension = "platform" + buildConfigField("String", "OPENIAP_STORE", "\"amazon\"") + } } buildTypes { diff --git a/packages/google/openiap/build.gradle.kts b/packages/google/openiap/build.gradle.kts index ba1b36ff..8a0d0072 100644 --- a/packages/google/openiap/build.gradle.kts +++ b/packages/google/openiap/build.gradle.kts @@ -1,6 +1,19 @@ import groovy.json.JsonSlurper +import java.io.File import org.jetbrains.kotlin.gradle.dsl.JvmTarget +fun locateOpeniapVersionsFile(startDir: File): File { + var current: File? = startDir + while (current != null) { + val candidate = File(current, "openiap-versions.json") + if (candidate.isFile) { + return candidate + } + current = current.parentFile + } + throw GradleException("packages/google: missing openiap-versions.json from ${startDir.absolutePath}") +} + plugins { id("com.android.library") id("org.jetbrains.kotlin.android") @@ -8,14 +21,18 @@ plugins { id("com.vanniktech.maven.publish") } -// Read version from monorepo root openiap-versions.json -val versionsFile = File(rootDir.parentFile.parentFile, "openiap-versions.json") -if (!versionsFile.isFile) { - error("packages/google: missing openiap-versions.json at ${versionsFile.path}") -} +// Read version from Gradle property first, then from monorepo root openiap-versions.json. +// Release and local publish scripts pass -P/ORG_GRADLE_PROJECT_openIapVersion; +// normal development builds use the repository SSOT file. +val versionsFile = locateOpeniapVersionsFile(projectDir) val versionsJson = JsonSlurper().parseText(versionsFile.readText()) as Map<*, *> -val openIapVersion: String = versionsJson["google"]?.toString() - ?: error("packages/google: 'google' version missing in openiap-versions.json") +val openIapVersion: String = project.findProperty("openIapVersion")?.toString()?.takeIf { it.isNotBlank() } + ?: versionsJson["google"]?.toString()?.takeIf { it.isNotBlank() } + ?: throw GradleException("packages/google: 'google' version missing in openiap-versions.json") +val isPublishTaskRequested = gradle.startParameter.taskNames.any { taskName -> + taskName.contains("publish", ignoreCase = true) || + taskName.contains("mavenCentral", ignoreCase = true) +} android { namespace = "io.github.hyochan.openiap" @@ -51,6 +68,11 @@ android { dimension = "platform" buildConfigField("String", "OPENIAP_STORE", "\"horizon\"") } + // Amazon flavor - Amazon Appstore SDK IAP only + create("amazon") { + dimension = "platform" + buildConfigField("String", "OPENIAP_STORE", "\"amazon\"") + } } compileOptions { @@ -75,12 +97,19 @@ android { named("horizon") { java.srcDirs("src/horizon/java") } + named("amazon") { + java.srcDirs("src/amazon/java") + manifest.srcFile("src/amazon/AndroidManifest.xml") + } named("testPlay") { java.srcDirs("src/testPlay/java") } named("testHorizon") { java.srcDirs("src/testHorizon/java") } + named("testAmazon") { + java.srcDirs("src/testAmazon/java") + } } testOptions { @@ -120,6 +149,10 @@ dependencies { add("horizonCompileOnly", "com.meta.horizon.billingclient.api:horizon-billing-compatibility:$horizonBillingCompatibilityVersion") add("horizonApi", "com.meta.horizon.billingclient.api:horizon-billing-compatibility:$horizonBillingCompatibilityVersion") + // Amazon flavor: Amazon Appstore SDK for Fire OS IAP + add("amazonCompileOnly", "com.amazon.device:amazon-appstore-sdk:3.0.8") + add("amazonApi", "com.amazon.device:amazon-appstore-sdk:3.0.8") + // Kotlin Coroutines implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") @@ -170,6 +203,22 @@ mavenPublishing { url.set("https://github.com/hyodotdev/openiap") } } + "amazon" -> { + coordinates(groupId, "openiap-google-amazon", openIapVersion) + + // Publish the Amazon flavor (Amazon Appstore SDK) + configure(com.vanniktech.maven.publish.AndroidSingleVariantLibrary( + variant = "amazonRelease", + sourcesJar = true, + publishJavadocJar = true + )) + + pom { + name.set("OpenIAP Amazon") + description.set("OpenIAP Android library using Amazon Appstore SDK IAP") + url.set("https://github.com/hyodotdev/openiap") + } + } else -> { // "play" is default coordinates(groupId, "openiap-google", openIapVersion) @@ -188,9 +237,11 @@ mavenPublishing { } } - // Central Portal is the default Maven Central target on Vanniktech 0.33+. - publishToMavenCentral() - signAllPublications() + if (isPublishTaskRequested) { + // Use the Central Portal publishing path only for publishing tasks. + publishToMavenCentral() + signAllPublications() + } pom { licenses { diff --git a/packages/google/openiap/src/amazon/AndroidManifest.xml b/packages/google/openiap/src/amazon/AndroidManifest.xml new file mode 100644 index 00000000..9dacfe9d --- /dev/null +++ b/packages/google/openiap/src/amazon/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapErrorExtensions.kt b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapErrorExtensions.kt new file mode 100644 index 00000000..2aa9923f --- /dev/null +++ b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapErrorExtensions.kt @@ -0,0 +1,26 @@ +package dev.hyo.openiap + +/** + * Compatibility mapping for tests and Android callers that still pass Google + * Billing-style response codes while using the Amazon flavor. + */ +@Suppress("DEPRECATION") +fun OpenIapError.Companion.fromBillingResponseCode( + responseCode: Int, + debugMessage: String? = null, +): OpenIapError { + return when (responseCode) { + 1 -> OpenIapError.UserCancelled(debugMessage) + 2 -> OpenIapError.ServiceUnavailable(debugMessage) + 3 -> OpenIapError.BillingUnavailable(debugMessage) + 4 -> OpenIapError.ItemUnavailable(debugMessage) + 5 -> OpenIapError.DeveloperError(debugMessage) + 6 -> OpenIapError.BillingError(debugMessage) + 7 -> OpenIapError.ItemAlreadyOwned(debugMessage) + 8 -> OpenIapError.ItemNotOwned(debugMessage) + -1 -> OpenIapError.ServiceDisconnected(debugMessage) + -2 -> OpenIapError.FeatureNotSupported(debugMessage) + -3 -> OpenIapError.ServiceTimeout(debugMessage) + else -> OpenIapError.UnknownError(debugMessage) + } +} diff --git a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt new file mode 100644 index 00000000..49b7a21f --- /dev/null +++ b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt @@ -0,0 +1,768 @@ +package dev.hyo.openiap + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.amazon.device.iap.PurchasingListener +import com.amazon.device.iap.PurchasingService +import com.amazon.device.iap.model.FulfillmentResult +import com.amazon.device.iap.model.ProductDataResponse +import com.amazon.device.iap.model.PurchaseResponse +import com.amazon.device.iap.model.PurchaseUpdatesResponse +import com.amazon.device.iap.model.UserData +import com.amazon.device.iap.model.UserDataResponse +import dev.hyo.openiap.helpers.onPurchaseError +import dev.hyo.openiap.helpers.onPurchaseUpdated +import dev.hyo.openiap.helpers.toAndroidPurchaseArgs +import dev.hyo.openiap.listener.DeveloperProvidedBillingListener +import dev.hyo.openiap.listener.OpenIapDeveloperProvidedBillingListener +import dev.hyo.openiap.listener.OpenIapPurchaseErrorListener +import dev.hyo.openiap.listener.OpenIapPurchaseUpdateListener +import dev.hyo.openiap.listener.OpenIapSubscriptionBillingIssueListener +import dev.hyo.openiap.listener.OpenIapUserChoiceBillingListener +import dev.hyo.openiap.listener.UserChoiceBillingListener +import dev.hyo.openiap.utils.verifyPurchaseWithIapkit +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import java.lang.ref.WeakReference +import java.util.concurrent.ConcurrentHashMap +import com.amazon.device.iap.model.Product as AmazonProduct +import com.amazon.device.iap.model.ProductType as AmazonProductType +import com.amazon.device.iap.model.Receipt as AmazonReceipt + +private const val TAG = "OpenIapAmazon" +private const val AMAZON_REQUEST_TIMEOUT_MS = 60_000L +private const val AMAZON_PRODUCT_DATA_BATCH_SIZE = 100 + +/** + * OpenIapModule for Amazon Appstore SDK IAP. + * + * Amazon's native IAP API is listener based instead of connection based. The + * OpenIAP connection lifecycle registers the listener and enables pending + * purchases, while individual API calls await the matching RequestId callback. + */ +class OpenIapModule( + private val context: Context, + @Suppress("UNUSED_PARAMETER") + private var alternativeBillingMode: AlternativeBillingMode = AlternativeBillingMode.NONE, + @Suppress("UNUSED_PARAMETER") + private var userChoiceBillingListener: UserChoiceBillingListener? = null, + @Suppress("UNUSED_PARAMETER") + private var developerProvidedBillingListener: DeveloperProvidedBillingListener? = null +) : OpenIapProtocol, PurchasingListener { + + constructor(context: Context, enableAlternativeBilling: Boolean) : this( + context, + if (enableAlternativeBilling) AlternativeBillingMode.ALTERNATIVE_ONLY else AlternativeBillingMode.NONE, + null + ) + + private var currentActivityRef: WeakReference? = null + private var isRegistered = false + private var storefrontCode: String = "" + + private val productDataRequests = ConcurrentHashMap>() + private val purchaseRequests = ConcurrentHashMap>() + private val purchaseUpdatesRequests = ConcurrentHashMap>() + private val userDataRequests = ConcurrentHashMap>() + private val earlyProductDataResponses = ConcurrentHashMap() + private val earlyPurchaseResponses = ConcurrentHashMap() + private val earlyPurchaseUpdatesResponses = ConcurrentHashMap() + private val earlyUserDataResponses = ConcurrentHashMap() + private val purchaseTypeByReceiptId = ConcurrentHashMap() + private val productTypeBySku = ConcurrentHashMap() + + private val purchaseUpdateListeners = ConcurrentHashMap.newKeySet() + private val purchaseErrorListeners = ConcurrentHashMap.newKeySet() + + private fun ensureRegistered() { + if (isRegistered) return + PurchasingService.registerListener(context.applicationContext, this) + runCatching { PurchasingService.enablePendingPurchases() } + .onFailure { OpenIapLog.w("Amazon pending purchases unavailable: ${it.message}", TAG) } + isRegistered = true + } + + override fun setActivity(activity: Activity?) { + currentActivityRef = activity?.let { WeakReference(it) } + } + + override val initConnection: MutationInitConnectionHandler = { + withContext(Dispatchers.Main) { + runCatching { + ensureRegistered() + val response = requestUserData() + when (response.requestStatus) { + UserDataResponse.RequestStatus.SUCCESSFUL -> true + UserDataResponse.RequestStatus.NOT_SUPPORTED -> { + OpenIapLog.w("Amazon initConnection not supported on this device", TAG) + false + } + UserDataResponse.RequestStatus.FAILED -> { + OpenIapLog.w("Amazon initConnection user data request failed", TAG) + false + } + } + }.getOrElse { error -> + OpenIapLog.e("Amazon initConnection failed: ${error.message}", error, TAG) + false + } + } + } + + override val endConnection: MutationEndConnectionHandler = { + withContext(Dispatchers.IO) { + productDataRequests.clear() + purchaseRequests.clear() + purchaseUpdatesRequests.clear() + userDataRequests.clear() + earlyProductDataResponses.clear() + earlyPurchaseResponses.clear() + earlyPurchaseUpdatesResponses.clear() + earlyUserDataResponses.clear() + purchaseTypeByReceiptId.clear() + productTypeBySku.clear() + storefrontCode = "" + true + } + } + + override val fetchProducts: QueryFetchProductsHandler = { params -> + withContext(Dispatchers.IO) { + val queryType = params.type ?: ProductQueryType.All + if (params.skus.isEmpty() && queryType != ProductQueryType.All) { + throw OpenIapError.EmptySkuList + } + + val responses = params.skus + .chunked(AMAZON_PRODUCT_DATA_BATCH_SIZE) + .map { requestProductData(it) } + val failedResponse = responses.firstOrNull { + it.requestStatus != ProductDataResponse.RequestStatus.SUCCESSFUL + } + + when (failedResponse?.requestStatus ?: ProductDataResponse.RequestStatus.SUCCESSFUL) { + ProductDataResponse.RequestStatus.SUCCESSFUL -> { + val products = responses.flatMap { response -> + response.productData.orEmpty().values + } + .sortedWith(compareBy { product -> + params.skus.indexOf(product.sku).takeIf { it >= 0 } ?: Int.MAX_VALUE + }) + products.forEach { productTypeBySku[it.sku] = it.productType } + + val inApps = products + .filter { it.productType != AmazonProductType.SUBSCRIPTION } + .map { it.toInAppProduct() } + val subscriptions = products + .filter { it.productType == AmazonProductType.SUBSCRIPTION } + .map { it.toSubscriptionProduct() } + + when (queryType) { + ProductQueryType.InApp -> FetchProductsResultProducts(inApps) + ProductQueryType.Subs -> FetchProductsResultSubscriptions(subscriptions) + ProductQueryType.All -> { + val allItems = buildList { + inApps.forEach { add(ProductOrSubscription.ProductItem(it)) } + subscriptions.forEach { add(ProductOrSubscription.ProductSubscriptionItem(it)) } + } + FetchProductsResultAll(allItems) + } + } + } + ProductDataResponse.RequestStatus.NOT_SUPPORTED -> { + throw OpenIapError.FeatureNotSupported("Amazon Appstore IAP is not supported on this device") + } + ProductDataResponse.RequestStatus.FAILED -> { + throw OpenIapError.QueryProduct.withDiagnostics( + debugMessage = "Amazon getProductData failed", + productIds = params.skus, + productType = queryType.rawValue, + isEmptyProductList = responses.all { it.productData.isNullOrEmpty() } + ) + } + } + } + } + + override val getAvailablePurchases: QueryGetAvailablePurchasesHandler = { + withContext(Dispatchers.IO) { + ensureRegistered() + requestPurchaseUpdates(reset = true) + } + } + + override val getActiveSubscriptions: QueryGetActiveSubscriptionsHandler = { subscriptionIds -> + withContext(Dispatchers.IO) { + val ids = subscriptionIds.orEmpty().toSet() + getAvailablePurchases(null) + .filterIsInstance() + .filter { purchase -> + purchase.isAutoRenewing && (ids.isEmpty() || purchase.productId in ids) + } + .map { purchase -> + ActiveSubscription( + autoRenewingAndroid = purchase.autoRenewingAndroid, + basePlanIdAndroid = purchase.currentPlanId, + currentPlanId = purchase.currentPlanId, + isActive = purchase.purchaseState == PurchaseState.Purchased, + productId = purchase.productId, + purchaseToken = purchase.purchaseToken, + purchaseTokenAndroid = purchase.purchaseToken, + transactionDate = purchase.transactionDate, + transactionId = purchase.transactionId ?: purchase.id + ) + } + } + } + + override val hasActiveSubscriptions: QueryHasActiveSubscriptionsHandler = { subscriptionIds -> + getActiveSubscriptions(subscriptionIds).isNotEmpty() + } + + override val requestPurchase: MutationRequestPurchaseHandler = { props -> + val purchases = withContext(Dispatchers.IO) { + ensureRegistered() + val androidArgs = props.toAndroidPurchaseArgs() + if (androidArgs.skus.isEmpty()) { + emitPurchaseError(OpenIapError.EmptySkuList) + return@withContext emptyList() + } + if (androidArgs.skus.size != 1) { + emitPurchaseError( + OpenIapError.DeveloperError("Amazon Appstore SDK purchases one SKU at a time") + ) + return@withContext emptyList() + } + + val sku = androidArgs.skus.first() + val response = requestAmazonPurchase(sku) + when (response.requestStatus) { + PurchaseResponse.RequestStatus.SUCCESSFUL -> { + val receipt = response.receipt ?: run { + emitPurchaseError(OpenIapError.PurchaseFailed("Amazon purchase response did not include a receipt")) + return@withContext emptyList() + } + val purchase = receipt.toPurchase() + purchaseTypeByReceiptId[receipt.receiptId] = receipt.productType + productTypeBySku[receipt.sku] = receipt.productType + purchaseUpdateListeners.forEach { listener -> + runCatching { listener.onPurchaseUpdated(purchase) } + } + listOf(purchase) + } + PurchaseResponse.RequestStatus.ALREADY_PURCHASED -> { + val error = OpenIapError.ItemAlreadyOwned("Amazon reported the item is already purchased") + emitPurchaseError(error) + emptyList() + } + PurchaseResponse.RequestStatus.INVALID_SKU -> { + val error = OpenIapError.SkuNotFound(sku) + emitPurchaseError(error) + emptyList() + } + PurchaseResponse.RequestStatus.NOT_SUPPORTED -> { + val error = OpenIapError.FeatureNotSupported("Amazon Appstore IAP is not supported on this device") + emitPurchaseError(error) + emptyList() + } + PurchaseResponse.RequestStatus.PENDING -> { + val error = OpenIapError.PurchaseDeferred + emitPurchaseError(error) + emptyList() + } + PurchaseResponse.RequestStatus.FAILED -> { + val error = OpenIapError.UserCancelled("Amazon purchase failed or was cancelled") + emitPurchaseError(error) + emptyList() + } + } + } + RequestPurchaseResultPurchases(purchases) + } + + suspend fun getAvailableItems(type: ProductQueryType): List = withContext(Dispatchers.IO) { + requestPurchaseUpdates(reset = true).filter { purchase -> + val receiptId = purchase.purchaseToken ?: purchase.id + val productType = purchaseTypeByReceiptId[receiptId] + ?: productTypeBySku[purchase.productId] + when (type) { + ProductQueryType.All -> true + ProductQueryType.Subs -> productType == AmazonProductType.SUBSCRIPTION + ProductQueryType.InApp -> productType != AmazonProductType.SUBSCRIPTION + } + } + } + + override val finishTransaction: MutationFinishTransactionHandler = { purchase, _ -> + withContext(Dispatchers.IO) { + ensureRegistered() + val receiptId = purchase.purchaseToken ?: purchase.id + if (receiptId.isBlank()) { + throw OpenIapError.PurchaseFailed("Missing Amazon receiptId") + } + PurchasingService.notifyFulfillment(receiptId, FulfillmentResult.FULFILLED) + } + } + + override val acknowledgePurchaseAndroid: MutationAcknowledgePurchaseAndroidHandler = { purchaseToken -> + withContext(Dispatchers.IO) { + runCatching { + ensureRegistered() + PurchasingService.notifyFulfillment(purchaseToken, FulfillmentResult.FULFILLED) + true + }.getOrElse { + OpenIapLog.w("Amazon acknowledge failed: ${it.message}", TAG) + false + } + } + } + + override val consumePurchaseAndroid: MutationConsumePurchaseAndroidHandler = { purchaseToken -> + withContext(Dispatchers.IO) { + runCatching { + ensureRegistered() + PurchasingService.notifyFulfillment(purchaseToken, FulfillmentResult.FULFILLED) + true + }.getOrElse { + OpenIapLog.w("Amazon consume failed: ${it.message}", TAG) + false + } + } + } + + override val restorePurchases: MutationRestorePurchasesHandler = { + withContext(Dispatchers.IO) { + requestPurchaseUpdates(reset = true) + Unit + } + } + + override val deepLinkToSubscriptions: MutationDeepLinkToSubscriptionsHandler = { + withContext(Dispatchers.Main) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("amzn://apps/android?p=${context.packageName}")) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + runCatching { context.startActivity(intent) } + .onFailure { OpenIapLog.w("Amazon subscription deep link unavailable: ${it.message}", TAG) } + Unit + } + } + + @Deprecated("Use verifyPurchase") + override val validateReceipt: MutationValidateReceiptHandler = { + verifyPurchase(it) + } + + override val verifyPurchase: MutationVerifyPurchaseHandler = { + throw OpenIapError.FeatureNotSupported( + "Amazon receipt verification requires server-side RVS integration" + ) + } + + override val verifyPurchaseWithProvider: MutationVerifyPurchaseWithProviderHandler = { + if (it.provider != PurchaseVerificationProvider.Iapkit) { + throw OpenIapError.FeatureNotSupported() + } + val options = it.iapkit ?: throw OpenIapError.DeveloperError( + "Missing IAPKit verification parameters" + ) + val amazon = options.amazon ?: throw OpenIapError.DeveloperError( + "Amazon IAPKit verification requires amazon parameters" + ) + val resolvedOptions = if (amazon.userId.isNullOrBlank()) { + val userDataResponse = requestUserData() + val userId = userDataResponse.userData?.userId + ?: throw OpenIapError.DeveloperError("Amazon IAPKit verification could not resolve userId") + options.copy(amazon = amazon.copy(userId = userId)) + } else { + options + } + + VerifyPurchaseWithProviderResult( + iapkit = verifyPurchaseWithIapkit(resolvedOptions, TAG), + provider = it.provider + ) + } + + private val purchaseError: SubscriptionPurchaseErrorHandler = { + onPurchaseError(this::addPurchaseErrorListener, this::removePurchaseErrorListener) + } + + private val purchaseUpdated: SubscriptionPurchaseUpdatedHandler = { + onPurchaseUpdated(this::addPurchaseUpdateListener, this::removePurchaseUpdateListener) + } + + private val subscriptionBillingIssue: SubscriptionSubscriptionBillingIssueHandler = { + // Amazon Appstore SDK does not expose a suspended-subscription event, so + // fail immediately instead of leaving consumers waiting forever. + throw OpenIapError.FeatureNotSupported() + } + + override val queryHandlers: QueryHandlers = QueryHandlers( + fetchProducts = fetchProducts, + getActiveSubscriptions = getActiveSubscriptions, + getAvailablePurchases = getAvailablePurchases, + getStorefront = { getStorefront() }, + getStorefrontIOS = { getStorefront() }, + hasActiveSubscriptions = hasActiveSubscriptions + ) + + @Suppress("DEPRECATION") + override val mutationHandlers: MutationHandlers = MutationHandlers( + acknowledgePurchaseAndroid = acknowledgePurchaseAndroid, + checkAlternativeBillingAvailabilityAndroid = { checkAlternativeBillingAvailability() }, + consumePurchaseAndroid = consumePurchaseAndroid, + createAlternativeBillingTokenAndroid = { createAlternativeBillingReportingToken() }, + createBillingProgramReportingDetailsAndroid = { program -> + createBillingProgramReportingDetails(program) + }, + deepLinkToSubscriptions = deepLinkToSubscriptions, + endConnection = endConnection, + finishTransaction = finishTransaction, + initConnection = initConnection, + isBillingProgramAvailableAndroid = { program -> isBillingProgramAvailable(program) }, + launchExternalLinkAndroid = { params -> + val activity = currentActivityRef?.get() + ?: throw OpenIapError.MissingCurrentActivity + launchExternalLink(activity, params) + }, + requestPurchase = requestPurchase, + restorePurchases = restorePurchases, + showAlternativeBillingDialogAndroid = { + val activity = currentActivityRef?.get() + ?: throw OpenIapError.MissingCurrentActivity + showAlternativeBillingInformationDialog(activity) + }, + validateReceipt = validateReceipt, + verifyPurchase = verifyPurchase, + verifyPurchaseWithProvider = verifyPurchaseWithProvider + ) + + override val subscriptionHandlers: SubscriptionHandlers = SubscriptionHandlers( + purchaseError = purchaseError, + purchaseUpdated = purchaseUpdated, + subscriptionBillingIssue = subscriptionBillingIssue + ) + + suspend fun getStorefront(): String = withContext(Dispatchers.IO) { + if (storefrontCode.isNotBlank()) return@withContext storefrontCode + runCatching { requestUserData() } + storefrontCode + } + + override fun addPurchaseUpdateListener(listener: OpenIapPurchaseUpdateListener) { + purchaseUpdateListeners.add(listener) + } + + override fun removePurchaseUpdateListener(listener: OpenIapPurchaseUpdateListener) { + purchaseUpdateListeners.remove(listener) + } + + override fun addPurchaseErrorListener(listener: OpenIapPurchaseErrorListener) { + purchaseErrorListeners.add(listener) + } + + override fun removePurchaseErrorListener(listener: OpenIapPurchaseErrorListener) { + purchaseErrorListeners.remove(listener) + } + + override fun addUserChoiceBillingListener(listener: OpenIapUserChoiceBillingListener) = Unit + + override fun removeUserChoiceBillingListener(listener: OpenIapUserChoiceBillingListener) = Unit + + override fun addDeveloperProvidedBillingListener(listener: OpenIapDeveloperProvidedBillingListener) = Unit + + override fun removeDeveloperProvidedBillingListener(listener: OpenIapDeveloperProvidedBillingListener) = Unit + + override fun addSubscriptionBillingIssueListener(listener: OpenIapSubscriptionBillingIssueListener) = Unit + + override fun removeSubscriptionBillingIssueListener(listener: OpenIapSubscriptionBillingIssueListener) = Unit + + override fun setUserChoiceBillingListener(listener: UserChoiceBillingListener?) { + userChoiceBillingListener = listener + } + + override fun setDeveloperProvidedBillingListener(listener: DeveloperProvidedBillingListener?) { + developerProvidedBillingListener = listener + } + + @Deprecated("Amazon Appstore does not support Google Play alternative billing") + override suspend fun checkAlternativeBillingAvailability(): Boolean = false + + @Deprecated("Amazon Appstore does not support Google Play alternative billing") + override suspend fun showAlternativeBillingInformationDialog(activity: Activity): Boolean = false + + @Deprecated("Amazon Appstore does not support Google Play alternative billing") + override suspend fun createAlternativeBillingReportingToken(): String? = null + + override suspend fun isBillingProgramAvailable( + program: BillingProgramAndroid + ): BillingProgramAvailabilityResultAndroid = BillingProgramAvailabilityResultAndroid( + billingProgram = program, + isAvailable = false + ) + + override suspend fun createBillingProgramReportingDetails( + program: BillingProgramAndroid + ): BillingProgramReportingDetailsAndroid { + throw OpenIapError.FeatureNotSupported("Amazon Appstore does not support Google Play billing programs") + } + + override suspend fun launchExternalLink( + activity: Activity, + params: LaunchExternalLinkParamsAndroid + ): Boolean = false + + override fun onUserDataResponse(userDataResponse: UserDataResponse) { + updateStorefront(userDataResponse.userData) + completeOrCache( + userDataRequests, + earlyUserDataResponses, + userDataResponse.requestId.toString(), + userDataResponse + ) + } + + override fun onProductDataResponse(productDataResponse: ProductDataResponse) { + completeOrCache( + productDataRequests, + earlyProductDataResponses, + productDataResponse.requestId.toString(), + productDataResponse + ) + } + + override fun onPurchaseResponse(purchaseResponse: PurchaseResponse) { + completeOrCache( + purchaseRequests, + earlyPurchaseResponses, + purchaseResponse.requestId.toString(), + purchaseResponse + ) + } + + override fun onPurchaseUpdatesResponse(purchaseUpdatesResponse: PurchaseUpdatesResponse) { + purchaseUpdatesResponse.receipts.orEmpty().forEach { receipt -> + purchaseTypeByReceiptId[receipt.receiptId] = receipt.productType + productTypeBySku[receipt.sku] = receipt.productType + } + completeOrCache( + purchaseUpdatesRequests, + earlyPurchaseUpdatesResponses, + purchaseUpdatesResponse.requestId.toString(), + purchaseUpdatesResponse + ) + } + + private fun emitPurchaseError(error: OpenIapError) { + purchaseErrorListeners.forEach { listener -> + runCatching { listener.onPurchaseError(error) } + } + } + + private suspend fun requestUserData(): UserDataResponse { + val requestId = withContext(Dispatchers.Main) { + ensureRegistered() + PurchasingService.getUserData().toString() + } + return awaitAmazonResponse(requestId, userDataRequests, earlyUserDataResponses) + } + + private suspend fun requestProductData(skus: List): ProductDataResponse { + val requestId = withContext(Dispatchers.Main) { + ensureRegistered() + PurchasingService.getProductData(skus.toSet()).toString() + } + return awaitAmazonResponse(requestId, productDataRequests, earlyProductDataResponses) + } + + private suspend fun requestAmazonPurchase(sku: String): PurchaseResponse { + val requestId = withContext(Dispatchers.Main) { + ensureRegistered() + PurchasingService.purchase(sku).toString() + } + return awaitAmazonResponse(requestId, purchaseRequests, earlyPurchaseResponses) + } + + private suspend fun requestPurchaseUpdates(reset: Boolean): List { + val purchases = mutableListOf() + var shouldReset = reset + do { + val response = awaitPurchaseUpdates(shouldReset) + shouldReset = false + when (response.requestStatus) { + PurchaseUpdatesResponse.RequestStatus.SUCCESSFUL -> { + purchases += response.receipts.orEmpty().map { receipt -> + purchaseTypeByReceiptId[receipt.receiptId] = receipt.productType + productTypeBySku[receipt.sku] = receipt.productType + receipt.toPurchase() + } + } + PurchaseUpdatesResponse.RequestStatus.NOT_SUPPORTED -> { + throw OpenIapError.FeatureNotSupported("Amazon Appstore IAP is not supported on this device") + } + PurchaseUpdatesResponse.RequestStatus.FAILED -> { + throw OpenIapError.RestoreFailed + } + } + } while (response.hasMore()) + return purchases + } + + private suspend fun awaitPurchaseUpdates(reset: Boolean): PurchaseUpdatesResponse { + val requestId = withContext(Dispatchers.Main) { + ensureRegistered() + PurchasingService.getPurchaseUpdates(reset).toString() + } + return awaitAmazonResponse(requestId, purchaseUpdatesRequests, earlyPurchaseUpdatesResponses) + } + + private suspend fun awaitAmazonResponse( + requestId: String, + pending: ConcurrentHashMap>, + earlyResponses: ConcurrentHashMap + ): T { + earlyResponses.remove(requestId)?.let { return it } + + val deferred = CompletableDeferred() + pending[requestId] = deferred + earlyResponses.remove(requestId)?.let { response -> + pending.remove(requestId) + if (!deferred.isCompleted) deferred.complete(response) + } + + return try { + withTimeout(AMAZON_REQUEST_TIMEOUT_MS) { deferred.await() } + } catch (_: TimeoutCancellationException) { + throw OpenIapError.ServiceTimeout("Amazon Appstore request timed out") + } finally { + pending.remove(requestId) + earlyResponses.remove(requestId) + } + } + + private fun completeOrCache( + pending: ConcurrentHashMap>, + earlyResponses: ConcurrentHashMap, + requestId: String, + value: T + ) { + val deferred = pending.remove(requestId) + if (deferred != null) { + if (!deferred.isCompleted) deferred.complete(value) + } else { + earlyResponses[requestId] = value + } + } + + private fun updateStorefront(userData: UserData?) { + val countryCode = userData?.let { + runCatching { + val method = it.javaClass.getMethod("getCountryCode") + method.invoke(it) as? String + }.getOrNull() + } + storefrontCode = countryCode + ?: userData?.marketplace + ?: storefrontCode + } + + private fun AmazonProduct.toInAppProduct(): ProductAndroid { + return ProductAndroid( + currency = "", + description = description.orEmpty(), + displayName = title, + displayPrice = price.orEmpty(), + id = sku, + nameAndroid = title.orEmpty(), + platform = IapPlatform.Android, + price = null, + productStatusAndroid = ProductStatusAndroid.Ok, + title = title.orEmpty(), + type = ProductType.InApp + ) + } + + private fun AmazonProduct.toSubscriptionProduct(): ProductSubscriptionAndroid { + val phase = PricingPhaseAndroid( + billingCycleCount = 0, + billingPeriod = reflectedString("getSubscriptionPeriod").orEmpty(), + formattedPrice = price.orEmpty(), + priceAmountMicros = "0", + priceCurrencyCode = "", + recurrenceMode = 1 + ) + val phases = PricingPhasesAndroid(listOf(phase)) + val legacyOffer = ProductSubscriptionAndroidOfferDetails( + basePlanId = sku, + offerId = null, + offerTags = emptyList(), + offerToken = "", + pricingPhases = phases + ) + val standardizedOffer = SubscriptionOffer( + basePlanIdAndroid = sku, + currency = "", + displayPrice = price.orEmpty(), + id = sku, + offerTagsAndroid = emptyList(), + offerTokenAndroid = "", + paymentMode = PaymentMode.PayAsYouGo, + period = null, + price = 0.0, + pricingPhasesAndroid = phases, + type = DiscountOfferType.Introductory + ) + return ProductSubscriptionAndroid( + currency = "", + description = description.orEmpty(), + displayName = title, + displayPrice = price.orEmpty(), + id = sku, + nameAndroid = title.orEmpty(), + platform = IapPlatform.Android, + price = null, + productStatusAndroid = ProductStatusAndroid.Ok, + subscriptionOfferDetailsAndroid = listOf(legacyOffer), + subscriptionOffers = listOf(standardizedOffer), + title = title.orEmpty(), + type = ProductType.Subs + ) + } + + private fun AmazonReceipt.toPurchase(): PurchaseAndroid { + val isSubscription = productType == AmazonProductType.SUBSCRIPTION + val dateMillis = purchaseDate?.time?.toDouble() ?: 0.0 + val state = if (isCanceled) PurchaseState.Unknown else PurchaseState.Purchased + return PurchaseAndroid( + autoRenewingAndroid = isSubscription && !isCanceled, + currentPlanId = if (isSubscription) sku else null, + dataAndroid = toJSON().toString(), + id = receiptId, + ids = listOf(sku), + isAcknowledgedAndroid = null, + isAutoRenewing = isSubscription && !isCanceled, + packageNameAndroid = context.packageName, + platform = IapPlatform.Android, + productId = sku, + purchaseState = state, + purchaseToken = receiptId, + quantity = 1, + signatureAndroid = null, + store = IapStore.Amazon, + transactionDate = dateMillis, + transactionId = receiptId + ) + } + + private fun AmazonProduct.reflectedString(methodName: String): String? { + return runCatching { + val method = javaClass.getMethod(methodName) + method.invoke(this) as? String + }.getOrNull() + } +} diff --git a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/store/OpenIapStoreExtensions.kt b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/store/OpenIapStoreExtensions.kt new file mode 100644 index 00000000..0fd0f068 --- /dev/null +++ b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/store/OpenIapStoreExtensions.kt @@ -0,0 +1,25 @@ +package dev.hyo.openiap.store + +import android.content.Context +import dev.hyo.openiap.AlternativeBillingMode +import dev.hyo.openiap.OpenIapModule +import dev.hyo.openiap.OpenIapProtocol +import dev.hyo.openiap.listener.UserChoiceBillingListener + +/** + * Amazon-specific extensions for OpenIapStore. + * These constructors are only available in the Amazon flavor. + */ + +/** + * Convenience constructor that creates OpenIapModule (Amazon flavor). + * + * @param context Android context + * @param alternativeBillingMode Ignored by Amazon; kept for API compatibility + * @param userChoiceBillingListener Ignored by Amazon; kept for API compatibility + */ +fun OpenIapStore( + context: Context, + alternativeBillingMode: AlternativeBillingMode = AlternativeBillingMode.NONE, + userChoiceBillingListener: UserChoiceBillingListener? = null +): OpenIapStore = OpenIapStore(OpenIapModule(context, alternativeBillingMode, userChoiceBillingListener) as OpenIapProtocol) diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt index c1674700..8cb1d8d8 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt @@ -576,7 +576,8 @@ public enum class IapStore(val rawValue: String) { Unknown("unknown"), Apple("apple"), Google("google"), - Horizon("horizon"); + Horizon("horizon"), + Amazon("amazon"); companion object { fun fromJson(value: String): IapStore = when (value) { @@ -588,6 +589,8 @@ public enum class IapStore(val rawValue: String) { "Google" -> IapStore.Google "horizon" -> IapStore.Horizon "Horizon" -> IapStore.Horizon + "amazon" -> IapStore.Amazon + "Amazon" -> IapStore.Amazon else -> throw IllegalArgumentException("Unknown IapStore value: $value") } } @@ -4500,7 +4503,8 @@ public data class RequestPurchaseProps( * * Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. * - apple: Always targets App Store - * - google: Targets Play Store by default, or Horizon when built with horizon flavor + * - google: Targets Play Store by default, Horizon when built with horizon flavor, + * or Fire OS when built with amazon flavor * (determined at build time, not runtime) */ public data class RequestPurchasePropsByPlatforms( @@ -4704,7 +4708,8 @@ public data class RequestSubscriptionIosProps( * * Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. * - apple: Always targets App Store - * - google: Targets Play Store by default, or Horizon when built with horizon flavor + * - google: Targets Play Store by default, Horizon when built with horizon flavor, + * or Fire OS when built with amazon flavor * (determined at build time, not runtime) */ public data class RequestSubscriptionPropsByPlatforms( @@ -4744,6 +4749,41 @@ public data class RequestSubscriptionPropsByPlatforms( ) } +public data class RequestVerifyPurchaseWithIapkitAmazonProps( + /** + * Amazon Appstore receipt id returned by PurchaseResponse.getReceipt().getReceiptId(). + */ + val receiptId: String, + /** + * Use Amazon RVS Cloud Sandbox for App Tester receipts. + */ + val sandbox: Boolean? = null, + /** + * Amazon Appstore user id returned by PurchaseResponse.getUserData().getUserId(). + */ + val userId: String? = null +) { + companion object { + fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitAmazonProps? { + val receiptId = json["receiptId"] as? String + val sandbox = json["sandbox"] as? Boolean + val userId = json["userId"] as? String + if (receiptId == null) return null + return RequestVerifyPurchaseWithIapkitAmazonProps( + receiptId = receiptId, + sandbox = sandbox, + userId = userId, + ) + } + } + + fun toJson(): Map = mapOf( + "receiptId" to receiptId, + "sandbox" to sandbox, + "userId" to userId, + ) +} + public data class RequestVerifyPurchaseWithIapkitAppleProps( /** * The JWS token returned with the purchase response. @@ -4791,8 +4831,13 @@ public data class RequestVerifyPurchaseWithIapkitGoogleProps( * * - apple: Verifies via App Store (JWS token) * - google: Verifies via Play Store (purchase token) + * - amazon: Verifies via Amazon Appstore RVS (userId + receiptId) */ public data class RequestVerifyPurchaseWithIapkitProps( + /** + * Amazon Appstore verification parameters. + */ + val amazon: RequestVerifyPurchaseWithIapkitAmazonProps? = null, /** * API key used for the Authorization header (Bearer {apiKey}). */ @@ -4809,6 +4854,7 @@ public data class RequestVerifyPurchaseWithIapkitProps( companion object { fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitProps { return RequestVerifyPurchaseWithIapkitProps( + amazon = (json["amazon"] as? Map)?.let { RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(it) }, apiKey = json["apiKey"] as? String, apple = (json["apple"] as? Map)?.let { RequestVerifyPurchaseWithIapkitAppleProps.fromJson(it) }, google = (json["google"] as? Map)?.let { RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(it) }, @@ -4817,6 +4863,7 @@ public data class RequestVerifyPurchaseWithIapkitProps( } fun toJson(): Map = mapOf( + "amazon" to amazon?.toJson(), "apiKey" to apiKey, "apple" to apple?.toJson(), "google" to google?.toJson(), diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt index 927063a8..ec23bfbe 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt @@ -57,7 +57,7 @@ import kotlinx.coroutines.launch /** * OpenIapStore (Android) - * Convenience store that wraps an [OpenIapProtocol] implementation (Play Store or Horizon) + * Convenience store that wraps an [OpenIapProtocol] implementation (Play, Horizon, or Amazon) * and exposes suspend APIs with observable StateFlows for UI layers to consume. */ class OpenIapStore(private val module: OpenIapProtocol) { @@ -748,6 +748,10 @@ private fun buildModule(context: Context, store: String?, appId: String?): OpenI OpenIapLog.d("Loading OpenIapModule (Horizon flavor)", "OpenIapStore") loadHorizonModule(context) } + "amazon", "fireos", "fire" -> { + OpenIapLog.d("Loading OpenIapModule (Amazon flavor)", "OpenIapStore") + loadAmazonModule(context) + } else -> { // Default to Play Store (includes "play", "google", "gplay", "googleplay", "gms") OpenIapLog.d("Loading OpenIapModule (Play flavor)", "OpenIapStore") @@ -787,6 +791,36 @@ private fun loadHorizonModule(context: Context): OpenIapProtocol { } } +/** + * Load OpenIapModule (Amazon flavor) via reflection + * Note: Amazon flavor uses the same package and class name as Play flavor + */ +private fun loadAmazonModule(context: Context): OpenIapProtocol { + return try { + val clazz = Class.forName("dev.hyo.openiap.OpenIapModule") + val alternativeBillingModeClass = Class.forName("dev.hyo.openiap.AlternativeBillingMode") + val userChoiceBillingListenerClass = Class.forName("dev.hyo.openiap.listener.UserChoiceBillingListener") + val developerProvidedBillingListenerClass = Class.forName("dev.hyo.openiap.listener.DeveloperProvidedBillingListener") + + val constructor = clazz.getConstructor( + Context::class.java, + alternativeBillingModeClass, + userChoiceBillingListenerClass, + developerProvidedBillingListenerClass + ) + + val noneMode = alternativeBillingModeClass.enumConstants?.first { + (it as Enum<*>).name == "NONE" + } + + val instance = constructor.newInstance(context, noneMode, null, null) as OpenIapProtocol + OpenIapLog.d("Successfully loaded OpenIapModule (Amazon flavor)", "OpenIapStore") + instance + } catch (e: Throwable) { + throw IllegalStateException("Failed to load OpenIapModule (Amazon flavor). Make sure you're using the Amazon flavor.", e) + } +} + /** * Load OpenIapModule (Play flavor) via reflection */ diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt index cf829a81..243befaf 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt @@ -170,14 +170,20 @@ suspend fun verifyPurchaseWithIapkit( ): RequestVerifyPurchaseWithIapkitResult = withContext(Dispatchers.IO) { val endpoint = DEFAULT_IAPKIT_ENDPOINT - // On Android, only Google verification is supported via IAPKit - // Note: Horizon verification requires direct S2S API calls to Meta (not yet supported) - if (props.google == null) { - throw IllegalArgumentException("IAPKit verification on Android requires google payload") + val hasGoogle = props.google != null + val hasAmazon = props.amazon != null + if (listOf(hasGoogle, hasAmazon).count { it } != 1) { + throw IllegalArgumentException( + "IAPKit verification on Android requires exactly one google or amazon payload" + ) } - val store = IapStore.Google - val payload = buildGooglePayload(props) + val store = if (hasAmazon) IapStore.Amazon else IapStore.Google + val payload = when (store) { + IapStore.Amazon -> buildAmazonPayload(props) + IapStore.Google -> buildGooglePayload(props) + else -> throw IllegalArgumentException("IAPKit verification on Android does not support ${store.rawValue}") + } val connection = connectionFactory(endpoint).apply { requestMethod = "POST" @@ -258,6 +264,26 @@ private fun buildGooglePayload(props: RequestVerifyPurchaseWithIapkitProps): Map ) } +/** + * Build payload for Amazon Appstore RVS verification via IAPKit. + */ +private fun buildAmazonPayload(props: RequestVerifyPurchaseWithIapkitProps): Map { + val amazon = props.amazon + ?: throw IllegalArgumentException("IAPKit Amazon verification requires amazon options") + val userId = amazon.userId?.trim().orEmpty() + val receiptId = amazon.receiptId.trim() + if (userId.isBlank() || receiptId.isBlank()) { + throw IllegalArgumentException("IAPKit Amazon verification requires userId and receiptId") + } + return mutableMapOf( + "store" to IapStore.Amazon.rawValue, + "userId" to userId, + "receiptId" to receiptId + ).apply { + amazon.sandbox?.let { put("sandbox", it) } + } +} + private fun String?.orElse(fallback: String): String = this ?: fallback diff --git a/packages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.kt b/packages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.kt index d9d691e3..f70aece1 100644 --- a/packages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.kt +++ b/packages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.kt @@ -6,6 +6,8 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test +private const val BILLING_RESPONSE_SERVICE_TIMEOUT = -3 + class OpenIapErrorTest { @Test @@ -317,7 +319,7 @@ class OpenIapErrorTest { assertTrue(OpenIapError.fromBillingResponseCode(BillingClient.BillingResponseCode.ITEM_NOT_OWNED) is OpenIapError.ItemNotOwned) assertTrue(OpenIapError.fromBillingResponseCode(BillingClient.BillingResponseCode.SERVICE_DISCONNECTED) is OpenIapError.ServiceDisconnected) assertTrue(OpenIapError.fromBillingResponseCode(BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED) is OpenIapError.FeatureNotSupported) - assertTrue(OpenIapError.fromBillingResponseCode(BillingClient.BillingResponseCode.SERVICE_TIMEOUT) is OpenIapError.ServiceTimeout) + assertTrue(OpenIapError.fromBillingResponseCode(BILLING_RESPONSE_SERVICE_TIMEOUT) is OpenIapError.ServiceTimeout) } @Test @@ -341,7 +343,7 @@ class OpenIapErrorTest { BillingClient.BillingResponseCode.ITEM_NOT_OWNED, BillingClient.BillingResponseCode.SERVICE_DISCONNECTED, BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED, - BillingClient.BillingResponseCode.SERVICE_TIMEOUT, + BILLING_RESPONSE_SERVICE_TIMEOUT, 999 // else branch → UnknownError ) codesToAssert.forEach { code -> diff --git a/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt b/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt index 549edd02..b5864f9f 100644 --- a/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt +++ b/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt @@ -6,6 +6,7 @@ import dev.hyo.openiap.utils.verifyPurchaseWithHorizon import dev.hyo.openiap.utils.verifyPurchaseWithIapkit import dev.hyo.openiap.IapStore import dev.hyo.openiap.IapkitPurchaseState +import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitAmazonProps import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitGoogleProps import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitProps import dev.hyo.openiap.VerifyPurchaseGoogleOptions @@ -150,18 +151,19 @@ class PurchaseVerificationValidatorTest { } @Test - fun `verifyPurchaseWithIapkit throws without google props`() = runTest { + fun `verifyPurchaseWithIapkit throws without android store props`() = runTest { val props = RequestVerifyPurchaseWithIapkitProps( apiKey = null, apple = null, - google = null + google = null, + amazon = null ) try { verifyPurchaseWithIapkit(props, "TEST") { _ -> throw AssertionError("Connection should not be created when google props are missing") } - throw AssertionError("Expected IllegalArgumentException for missing google props") + throw AssertionError("Expected IllegalArgumentException for missing android store props") } catch (expected: IllegalArgumentException) { // Expected path } @@ -174,7 +176,8 @@ class PurchaseVerificationValidatorTest { apple = null, google = RequestVerifyPurchaseWithIapkitGoogleProps( purchaseToken = "token-abc" - ) + ), + amazon = null ) verifyPurchaseWithIapkit(props, "TEST") { _ -> @@ -189,7 +192,8 @@ class PurchaseVerificationValidatorTest { apple = null, google = RequestVerifyPurchaseWithIapkitGoogleProps( purchaseToken = "" - ) + ), + amazon = null ) try { @@ -209,7 +213,8 @@ class PurchaseVerificationValidatorTest { apple = null, google = RequestVerifyPurchaseWithIapkitGoogleProps( purchaseToken = "token-123" - ) + ), + amazon = null ) val connection = FakeHttpURLConnection(200, """{"store":"google","isValid":true,"state":"ENTITLED"}""") @@ -231,7 +236,8 @@ class PurchaseVerificationValidatorTest { apple = null, google = RequestVerifyPurchaseWithIapkitGoogleProps( purchaseToken = "token-123" - ) + ), + amazon = null ) val connection = FakeHttpURLConnection(200, """{"store":"google","isValid":false,"state":"INAUTHENTIC"}""") @@ -246,15 +252,92 @@ class PurchaseVerificationValidatorTest { } @Test - fun `verifyPurchaseWithIapkit wraps non-2xx as PurchaseVerificationFailed`() = runTest { + fun `verifyPurchaseWithIapkit posts amazon receipt details`() = runTest { + val props = RequestVerifyPurchaseWithIapkitProps( + apiKey = "secret", + apple = null, + google = null, + amazon = RequestVerifyPurchaseWithIapkitAmazonProps( + userId = "amzn1.account.ABC123", + receiptId = "amzn1.receipt.ABC123456789", + sandbox = true + ) + ) + + val connection = FakeHttpURLConnection(200, """{"store":"amazon","isValid":true,"state":"ENTITLED"}""") + val result = verifyPurchaseWithIapkit(props, "TEST") { _ -> connection } + + assertEquals(IapStore.Amazon, result.store) + assertTrue(result.isValid) + assertEquals(IapkitPurchaseState.Entitled, result.state) + assertEquals("Bearer secret", connection.headers["Authorization"]) + + val bodyMap = Gson().fromJson(requireNotNull(connection.writtenBody), Map::class.java) as Map<*, *> + assertEquals("amazon", bodyMap["store"]) + assertEquals("amzn1.account.ABC123", bodyMap["userId"]) + assertEquals("amzn1.receipt.ABC123456789", bodyMap["receiptId"]) + assertEquals(true, bodyMap["sandbox"]) + } + + @Test + fun `verifyPurchaseWithIapkit throws when amazon payload is incomplete`() = runTest { + val props = RequestVerifyPurchaseWithIapkitProps( + apiKey = null, + apple = null, + google = null, + amazon = RequestVerifyPurchaseWithIapkitAmazonProps( + userId = "", + receiptId = "amzn1.receipt.ABC123456789", + sandbox = false + ) + ) + + try { + verifyPurchaseWithIapkit(props, "TEST") { _ -> + throw AssertionError("Connection should not be created when amazon payload is invalid") + } + throw AssertionError("Expected IllegalArgumentException for invalid amazon payload") + } catch (expected: IllegalArgumentException) { + // Expected path + } + } + + @Test + fun `verifyPurchaseWithIapkit throws when multiple android payloads are provided`() = runTest { val props = RequestVerifyPurchaseWithIapkitProps( apiKey = null, apple = null, google = RequestVerifyPurchaseWithIapkitGoogleProps( purchaseToken = "token-123" + ), + amazon = RequestVerifyPurchaseWithIapkitAmazonProps( + userId = "amzn1.account.ABC123", + receiptId = "amzn1.receipt.ABC123456789", + sandbox = false ) ) + try { + verifyPurchaseWithIapkit(props, "TEST") { _ -> + throw AssertionError("Connection should not be created when multiple payloads are provided") + } + throw AssertionError("Expected IllegalArgumentException for multiple payloads") + } catch (expected: IllegalArgumentException) { + // Expected path + } + } + + @Test + fun `verifyPurchaseWithIapkit wraps non-2xx as PurchaseVerificationFailed`() = runTest { + val props = RequestVerifyPurchaseWithIapkitProps( + apiKey = null, + apple = null, + google = RequestVerifyPurchaseWithIapkitGoogleProps( + purchaseToken = "token-123" + ), + amazon = null + ) + try { verifyPurchaseWithIapkit( props, diff --git a/packages/google/openiap/src/testAmazon/java/dev/hyo/openiap/FetchProductsAmazonTest.kt b/packages/google/openiap/src/testAmazon/java/dev/hyo/openiap/FetchProductsAmazonTest.kt new file mode 100644 index 00000000..2fff35a3 --- /dev/null +++ b/packages/google/openiap/src/testAmazon/java/dev/hyo/openiap/FetchProductsAmazonTest.kt @@ -0,0 +1,40 @@ +package dev.hyo.openiap + +import android.content.ContextWrapper +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertTrue +import org.junit.Test + +class FetchProductsAmazonTest { + + @Test + fun `empty sku list with null type returns empty all result`() = runBlocking { + val module = OpenIapModule(ContextWrapper(null)) + + val result = module.fetchProducts(ProductRequest(emptyList(), null)) + + assertTrue(result is FetchProductsResultAll) + assertTrue((result as FetchProductsResultAll).value.orEmpty().isEmpty()) + } + + @Test + fun `empty sku list with all type returns empty all result`() = runBlocking { + val module = OpenIapModule(ContextWrapper(null)) + + val result = module.fetchProducts(ProductRequest(emptyList(), ProductQueryType.All)) + + assertTrue(result is FetchProductsResultAll) + assertTrue((result as FetchProductsResultAll).value.orEmpty().isEmpty()) + } + + @Test + fun `empty sku list with product type throws EmptySkuList`() = runBlocking { + val module = OpenIapModule(ContextWrapper(null)) + + val thrown = runCatching { + module.fetchProducts(ProductRequest(emptyList(), ProductQueryType.InApp)) + }.exceptionOrNull() + + assertTrue(thrown is OpenIapError.EmptySkuList) + } +} diff --git a/packages/google/openiap/src/testAmazon/java/dev/hyo/openiap/SubscriptionBillingIssueAmazonTest.kt b/packages/google/openiap/src/testAmazon/java/dev/hyo/openiap/SubscriptionBillingIssueAmazonTest.kt new file mode 100644 index 00000000..3f3fa1e1 --- /dev/null +++ b/packages/google/openiap/src/testAmazon/java/dev/hyo/openiap/SubscriptionBillingIssueAmazonTest.kt @@ -0,0 +1,52 @@ +package dev.hyo.openiap + +import android.content.ContextWrapper +import dev.hyo.openiap.listener.OpenIapSubscriptionBillingIssueListener +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.util.concurrent.atomic.AtomicInteger + +/** + * Amazon Appstore SDK has no subscription billing issue callback. The bundle + * still exposes the handler for API parity, but it must fail fast instead of + * suspending forever. + */ +class SubscriptionBillingIssueAmazonTest { + + @Test + fun `Amazon subscriptionBillingIssue handler throws FeatureNotSupported`() { + val module = OpenIapModule(ContextWrapper(null)) + + val handler = module.subscriptionHandlers.subscriptionBillingIssue + assertNotNull( + "Amazon flavor must wire subscriptionBillingIssue handler for bundle parity", + handler + ) + + val thrown = runCatching { runBlocking { withTimeout(5_000) { handler!!.invoke() } } }.exceptionOrNull() + assertTrue( + "Amazon subscriptionBillingIssue handler must throw FeatureNotSupported, got: $thrown", + thrown is OpenIapError.FeatureNotSupported + ) + } + + @Test + fun `add and remove SubscriptionBillingIssueListener never invokes the callback`() { + val module = OpenIapModule(ContextWrapper(null)) + val invoked = AtomicInteger(0) + val listener = OpenIapSubscriptionBillingIssueListener { invoked.incrementAndGet() } + + module.addSubscriptionBillingIssueListener(listener) + module.removeSubscriptionBillingIssueListener(listener) + + assertEquals( + "Amazon flavor must never invoke the subscriptionBillingIssue listener", + 0, + invoked.get() + ) + } +} diff --git a/packages/google/package.json b/packages/google/package.json index 035d7f12..19e3c9b7 100644 --- a/packages/google/package.json +++ b/packages/google/package.json @@ -1,6 +1,6 @@ { "name": "@hyodotdev/openiap-android", - "version": "2.2.1", + "version": "2.3.0-rc.1", "private": true, "description": "OpenIAP Android/Kotlin implementation", "scripts": { diff --git a/packages/gql/codegen/core/schema-linter.ts b/packages/gql/codegen/core/schema-linter.ts index 6423f7bf..a46eb91d 100644 --- a/packages/gql/codegen/core/schema-linter.ts +++ b/packages/gql/codegen/core/schema-linter.ts @@ -24,15 +24,40 @@ export interface LintOptions { strict?: boolean; } -const PLATFORM_TYPE_SUFFIX_EXCEPTIONS = new Set([ - // Public API names kept for source/binary compatibility. +const IOS_TYPE_SUFFIX_EXCEPTIONS = new Set([ + // StoreKit names this payload AppTransaction; keep the public OpenIAP type stable. 'AppTransaction', - 'ProductAndroidOneTimePurchaseOfferDetail', - 'ProductSubscriptionAndroidOfferDetails', +]); + +const ANDROID_TYPE_SUFFIX_EXCEPTIONS = new Set([ + // Legacy public type name from the User Choice Billing API. 'UserChoiceBillingDetails', + // Meta Horizon verification result is Android-flavor specific but keeps the + // store suffix for API clarity. 'VerifyPurchaseResultHorizon', ]); +function isAllowedPlatformTypeName( + typeName: string, + platform: 'ios' | 'android', +): boolean { + if (typeName.startsWith('Query') || typeName.startsWith('Mutation')) { + return true; + } + + if (platform === 'ios') { + return typeName.endsWith('IOS') || IOS_TYPE_SUFFIX_EXCEPTIONS.has(typeName); + } + + return ( + typeName.endsWith('Android') || + typeName.includes('Android') || + typeName.endsWith('Horizon') || + typeName.endsWith('Amazon') || + ANDROID_TYPE_SUFFIX_EXCEPTIONS.has(typeName) + ); +} + /** * Lint schema conventions and return findings. */ @@ -71,28 +96,24 @@ export function lintSchema( } // Platform suffix checks for types in platform-specific files - if (isIOSFile && !typeName.endsWith('IOS') && !typeName.startsWith('Query') && !typeName.startsWith('Mutation')) { - if (!PLATFORM_TYPE_SUFFIX_EXCEPTIONS.has(typeName)) { - results.push({ - level: 'warning', - file: fileName, - line: lineNum, - message: `Type "${typeName}" in iOS file should end with "IOS" suffix`, - rule: 'ios-type-suffix', - }); - } + if (isIOSFile && !isAllowedPlatformTypeName(typeName, 'ios')) { + results.push({ + level: 'warning', + file: fileName, + line: lineNum, + message: `Type "${typeName}" in iOS file should end with "IOS" suffix`, + rule: 'ios-type-suffix', + }); } - if (isAndroidFile && !typeName.endsWith('Android') && !typeName.startsWith('Query') && !typeName.startsWith('Mutation')) { - if (!PLATFORM_TYPE_SUFFIX_EXCEPTIONS.has(typeName)) { - results.push({ - level: 'warning', - file: fileName, - line: lineNum, - message: `Type "${typeName}" in Android file should end with "Android" suffix`, - rule: 'android-type-suffix', - }); - } + if (isAndroidFile && !isAllowedPlatformTypeName(typeName, 'android')) { + results.push({ + level: 'warning', + file: fileName, + line: lineNum, + message: `Type "${typeName}" in Android file should end with "Android" suffix`, + rule: 'android-type-suffix', + }); } continue; diff --git a/packages/gql/src/generated/Types.cs b/packages/gql/src/generated/Types.cs index c35b1bd3..05aab794 100644 --- a/packages/gql/src/generated/Types.cs +++ b/packages/gql/src/generated/Types.cs @@ -978,7 +978,8 @@ public enum IapStore Unknown, Apple, Google, - Horizon + Horizon, + Amazon } public sealed class IapStoreJsonConverter : JsonConverter @@ -997,6 +998,9 @@ public sealed class IapStoreJsonConverter : JsonConverter ["horizon"] = IapStore.Horizon, ["HORIZON"] = IapStore.Horizon, ["Horizon"] = IapStore.Horizon, + ["amazon"] = IapStore.Amazon, + ["AMAZON"] = IapStore.Amazon, + ["Amazon"] = IapStore.Amazon, }; private static readonly Dictionary _toString = new() @@ -1005,6 +1009,7 @@ public sealed class IapStoreJsonConverter : JsonConverter [IapStore.Apple] = "apple", [IapStore.Google] = "google", [IapStore.Horizon] = "horizon", + [IapStore.Amazon] = "amazon", }; public override IapStore Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -3846,7 +3851,8 @@ public sealed record RequestPurchaseProps /// /// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. /// - apple: Always targets App Store -/// - google: Targets Play Store by default, or Horizon when built with horizon flavor +/// - google: Targets Play Store by default, Horizon when built with horizon flavor, +/// or Fire OS when built with amazon flavor /// (determined at build time, not runtime) public sealed record RequestPurchasePropsByPlatforms { @@ -3943,7 +3949,8 @@ public sealed record RequestSubscriptionIosProps /// /// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. /// - apple: Always targets App Store -/// - google: Targets Play Store by default, or Horizon when built with horizon flavor +/// - google: Targets Play Store by default, Horizon when built with horizon flavor, +/// or Fire OS when built with amazon flavor /// (determined at build time, not runtime) public sealed record RequestSubscriptionPropsByPlatforms { @@ -3961,6 +3968,19 @@ public sealed record RequestSubscriptionPropsByPlatforms public RequestSubscriptionAndroidProps? Android { get; init; } } +public sealed record RequestVerifyPurchaseWithIapkitAmazonProps +{ + /// Amazon Appstore user id returned by PurchaseResponse.getUserData().getUserId(). + [JsonPropertyName("userId")] + public string? UserId { get; init; } + /// Amazon Appstore receipt id returned by PurchaseResponse.getReceipt().getReceiptId(). + [JsonPropertyName("receiptId")] + public required string ReceiptId { get; init; } + /// Use Amazon RVS Cloud Sandbox for App Tester receipts. + [JsonPropertyName("sandbox")] + public bool? Sandbox { get; init; } +} + public sealed record RequestVerifyPurchaseWithIapkitAppleProps { /// The JWS token returned with the purchase response. @@ -3979,6 +3999,7 @@ public sealed record RequestVerifyPurchaseWithIapkitGoogleProps /// /// - apple: Verifies via App Store (JWS token) /// - google: Verifies via Play Store (purchase token) +/// - amazon: Verifies via Amazon Appstore RVS (userId + receiptId) public sealed record RequestVerifyPurchaseWithIapkitProps { /// API key used for the Authorization header (Bearer {apiKey}). @@ -3990,6 +4011,9 @@ public sealed record RequestVerifyPurchaseWithIapkitProps /// Google Play Store verification parameters. [JsonPropertyName("google")] public RequestVerifyPurchaseWithIapkitGoogleProps? Google { get; init; } + /// Amazon Appstore verification parameters. + [JsonPropertyName("amazon")] + public RequestVerifyPurchaseWithIapkitAmazonProps? Amazon { get; init; } } /// Product-level subscription replacement parameters (Android) diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index be4d7f6d..3251b5c8 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -627,7 +627,8 @@ public enum class IapStore(val rawValue: String) { Unknown("unknown"), Apple("apple"), Google("google"), - Horizon("horizon") + Horizon("horizon"), + Amazon("amazon") companion object { fun fromJson(value: String): IapStore = when (value) { @@ -643,6 +644,9 @@ public enum class IapStore(val rawValue: String) { "horizon" -> IapStore.Horizon "HORIZON" -> IapStore.Horizon "Horizon" -> IapStore.Horizon + "amazon" -> IapStore.Amazon + "AMAZON" -> IapStore.Amazon + "Amazon" -> IapStore.Amazon else -> throw IllegalArgumentException("Unknown IapStore value: $value") } } @@ -4619,7 +4623,8 @@ public data class RequestPurchaseProps( * * Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. * - apple: Always targets App Store - * - google: Targets Play Store by default, or Horizon when built with horizon flavor + * - google: Targets Play Store by default, Horizon when built with horizon flavor, + * or Fire OS when built with amazon flavor * (determined at build time, not runtime) */ public data class RequestPurchasePropsByPlatforms( @@ -4823,7 +4828,8 @@ public data class RequestSubscriptionIosProps( * * Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. * - apple: Always targets App Store - * - google: Targets Play Store by default, or Horizon when built with horizon flavor + * - google: Targets Play Store by default, Horizon when built with horizon flavor, + * or Fire OS when built with amazon flavor * (determined at build time, not runtime) */ public data class RequestSubscriptionPropsByPlatforms( @@ -4863,6 +4869,41 @@ public data class RequestSubscriptionPropsByPlatforms( ) } +public data class RequestVerifyPurchaseWithIapkitAmazonProps( + /** + * Amazon Appstore receipt id returned by PurchaseResponse.getReceipt().getReceiptId(). + */ + val receiptId: String, + /** + * Use Amazon RVS Cloud Sandbox for App Tester receipts. + */ + val sandbox: Boolean? = null, + /** + * Amazon Appstore user id returned by PurchaseResponse.getUserData().getUserId(). + */ + val userId: String? = null +) { + companion object { + fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitAmazonProps? { + val receiptId = json["receiptId"] as? String + val sandbox = json["sandbox"] as? Boolean + val userId = json["userId"] as? String + if (receiptId == null) return null + return RequestVerifyPurchaseWithIapkitAmazonProps( + receiptId = receiptId, + sandbox = sandbox, + userId = userId, + ) + } + } + + fun toJson(): Map = mapOf( + "receiptId" to receiptId, + "sandbox" to sandbox, + "userId" to userId, + ) +} + public data class RequestVerifyPurchaseWithIapkitAppleProps( /** * The JWS token returned with the purchase response. @@ -4910,8 +4951,13 @@ public data class RequestVerifyPurchaseWithIapkitGoogleProps( * * - apple: Verifies via App Store (JWS token) * - google: Verifies via Play Store (purchase token) + * - amazon: Verifies via Amazon Appstore RVS (userId + receiptId) */ public data class RequestVerifyPurchaseWithIapkitProps( + /** + * Amazon Appstore verification parameters. + */ + val amazon: RequestVerifyPurchaseWithIapkitAmazonProps? = null, /** * API key used for the Authorization header (Bearer {apiKey}). */ @@ -4928,6 +4974,7 @@ public data class RequestVerifyPurchaseWithIapkitProps( companion object { fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitProps { return RequestVerifyPurchaseWithIapkitProps( + amazon = (json["amazon"] as? Map)?.let { RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(it) }, apiKey = json["apiKey"] as? String, apple = (json["apple"] as? Map)?.let { RequestVerifyPurchaseWithIapkitAppleProps.fromJson(it) }, google = (json["google"] as? Map)?.let { RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(it) }, @@ -4936,6 +4983,7 @@ public data class RequestVerifyPurchaseWithIapkitProps( } fun toJson(): Map = mapOf( + "amazon" to amazon?.toJson(), "apiKey" to apiKey, "apple" to apple?.toJson(), "google" to google?.toJson(), diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift index 0199cfc7..cecc3c52 100644 --- a/packages/gql/src/generated/Types.swift +++ b/packages/gql/src/generated/Types.swift @@ -303,6 +303,7 @@ public enum IapStore: String, Codable, CaseIterable { case apple = "apple" case google = "google" case horizon = "horizon" + case amazon = "amazon" } /// Payment mode for subscription offers. @@ -1847,7 +1848,8 @@ public struct RequestPurchaseProps: Codable { /// /// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. /// - apple: Always targets App Store -/// - google: Targets Play Store by default, or Horizon when built with horizon flavor +/// - google: Targets Play Store by default, Horizon when built with horizon flavor, +/// or Fire OS when built with amazon flavor /// (determined at build time, not runtime) public struct RequestPurchasePropsByPlatforms: Codable { /// @deprecated Use google instead @@ -1975,7 +1977,8 @@ public struct RequestSubscriptionIosProps: Codable { /// /// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. /// - apple: Always targets App Store -/// - google: Targets Play Store by default, or Horizon when built with horizon flavor +/// - google: Targets Play Store by default, Horizon when built with horizon flavor, +/// or Fire OS when built with amazon flavor /// (determined at build time, not runtime) public struct RequestSubscriptionPropsByPlatforms: Codable { /// @deprecated Use google instead @@ -2000,6 +2003,25 @@ public struct RequestSubscriptionPropsByPlatforms: Codable { } } +public struct RequestVerifyPurchaseWithIapkitAmazonProps: Codable { + /// Amazon Appstore receipt id returned by PurchaseResponse.getReceipt().getReceiptId(). + public var receiptId: String + /// Use Amazon RVS Cloud Sandbox for App Tester receipts. + public var sandbox: Bool? + /// Amazon Appstore user id returned by PurchaseResponse.getUserData().getUserId(). + public var userId: String? + + public init( + receiptId: String, + sandbox: Bool? = nil, + userId: String? = nil + ) { + self.receiptId = receiptId + self.sandbox = sandbox + self.userId = userId + } +} + public struct RequestVerifyPurchaseWithIapkitAppleProps: Codable { /// The JWS token returned with the purchase response. public var jws: String @@ -2026,7 +2048,10 @@ public struct RequestVerifyPurchaseWithIapkitGoogleProps: Codable { /// /// - apple: Verifies via App Store (JWS token) /// - google: Verifies via Play Store (purchase token) +/// - amazon: Verifies via Amazon Appstore RVS (userId + receiptId) public struct RequestVerifyPurchaseWithIapkitProps: Codable { + /// Amazon Appstore verification parameters. + public var amazon: RequestVerifyPurchaseWithIapkitAmazonProps? /// API key used for the Authorization header (Bearer {apiKey}). public var apiKey: String? /// Apple App Store verification parameters. @@ -2035,10 +2060,12 @@ public struct RequestVerifyPurchaseWithIapkitProps: Codable { public var google: RequestVerifyPurchaseWithIapkitGoogleProps? public init( + amazon: RequestVerifyPurchaseWithIapkitAmazonProps? = nil, apiKey: String? = nil, apple: RequestVerifyPurchaseWithIapkitAppleProps? = nil, google: RequestVerifyPurchaseWithIapkitGoogleProps? = nil ) { + self.amazon = amazon self.apiKey = apiKey self.apple = apple self.google = google diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index 9723c028..1f93a65f 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -534,7 +534,8 @@ enum IapStore { Unknown('unknown'), Apple('apple'), Google('google'), - Horizon('horizon'); + Horizon('horizon'), + Amazon('amazon'); const IapStore(this.value); final String value; @@ -550,6 +551,8 @@ enum IapStore { return IapStore.Google; case 'horizon': return IapStore.Horizon; + case 'amazon': + return IapStore.Amazon; } throw ArgumentError('Unknown IapStore value: $value'); } @@ -4540,7 +4543,8 @@ class _SubsPurchase extends RequestPurchaseProps { /// /// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. /// - apple: Always targets App Store -/// - google: Targets Play Store by default, or Horizon when built with horizon flavor +/// - google: Targets Play Store by default, Horizon when built with horizon flavor, +/// or Fire OS when built with amazon flavor /// (determined at build time, not runtime) class RequestPurchasePropsByPlatforms { const RequestPurchasePropsByPlatforms({ @@ -4717,7 +4721,8 @@ class RequestSubscriptionIosProps { /// /// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. /// - apple: Always targets App Store -/// - google: Targets Play Store by default, or Horizon when built with horizon flavor +/// - google: Targets Play Store by default, Horizon when built with horizon flavor, +/// or Fire OS when built with amazon flavor /// (determined at build time, not runtime) class RequestSubscriptionPropsByPlatforms { const RequestSubscriptionPropsByPlatforms({ @@ -4755,6 +4760,37 @@ class RequestSubscriptionPropsByPlatforms { } } +class RequestVerifyPurchaseWithIapkitAmazonProps { + const RequestVerifyPurchaseWithIapkitAmazonProps({ + required this.receiptId, + this.sandbox, + this.userId, + }); + + /// Amazon Appstore receipt id returned by PurchaseResponse.getReceipt().getReceiptId(). + final String receiptId; + /// Use Amazon RVS Cloud Sandbox for App Tester receipts. + final bool? sandbox; + /// Amazon Appstore user id returned by PurchaseResponse.getUserData().getUserId(). + final String? userId; + + factory RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(Map json) { + return RequestVerifyPurchaseWithIapkitAmazonProps( + receiptId: json['receiptId'] as String, + sandbox: json['sandbox'] as bool?, + userId: json['userId'] as String?, + ); + } + + Map toJson() { + return { + 'receiptId': receiptId, + 'sandbox': sandbox, + 'userId': userId, + }; + } +} + class RequestVerifyPurchaseWithIapkitAppleProps { const RequestVerifyPurchaseWithIapkitAppleProps({ required this.jws, @@ -4801,13 +4837,17 @@ class RequestVerifyPurchaseWithIapkitGoogleProps { /// /// - apple: Verifies via App Store (JWS token) /// - google: Verifies via Play Store (purchase token) +/// - amazon: Verifies via Amazon Appstore RVS (userId + receiptId) class RequestVerifyPurchaseWithIapkitProps { const RequestVerifyPurchaseWithIapkitProps({ + this.amazon, this.apiKey, this.apple, this.google, }); + /// Amazon Appstore verification parameters. + final RequestVerifyPurchaseWithIapkitAmazonProps? amazon; /// API key used for the Authorization header (Bearer {apiKey}). final String? apiKey; /// Apple App Store verification parameters. @@ -4817,6 +4857,7 @@ class RequestVerifyPurchaseWithIapkitProps { factory RequestVerifyPurchaseWithIapkitProps.fromJson(Map json) { return RequestVerifyPurchaseWithIapkitProps( + amazon: json['amazon'] != null ? RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(json['amazon'] as Map) : null, apiKey: json['apiKey'] as String?, apple: json['apple'] != null ? RequestVerifyPurchaseWithIapkitAppleProps.fromJson(json['apple'] as Map) : null, google: json['google'] != null ? RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(json['google'] as Map) : null, @@ -4825,6 +4866,7 @@ class RequestVerifyPurchaseWithIapkitProps { Map toJson() { return { + 'amazon': amazon?.toJson(), 'apiKey': apiKey, 'apple': apple?.toJson(), 'google': google?.toJson(), diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd index 76bc09e7..802cf309 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -182,6 +182,7 @@ enum IapStore { APPLE = 1, GOOGLE = 2, HORIZON = 3, + AMAZON = 4, } ## Payment mode for subscription offers. Determines how the user pays during the offer period. @@ -4029,7 +4030,7 @@ class RequestPurchaseProps: dict["useAlternativeBilling"] = use_alternative_billing return dict -## Platform-specific purchase request parameters. Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. - apple: Always targets App Store - google: Targets Play Store by default, or Horizon when built with horizon flavor (determined at build time, not runtime) +## Platform-specific purchase request parameters. Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. - apple: Always targets App Store - google: Targets Play Store by default, Horizon when built with horizon flavor, or Fire OS when built with amazon flavor (determined at build time, not runtime) class RequestPurchasePropsByPlatforms: ## Apple-specific purchase parameters var apple: RequestPurchaseIosProps @@ -4254,7 +4255,7 @@ class RequestSubscriptionIosProps: dict["advancedCommerceData"] = advanced_commerce_data return dict -## Platform-specific subscription request parameters. Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. - apple: Always targets App Store - google: Targets Play Store by default, or Horizon when built with horizon flavor (determined at build time, not runtime) +## Platform-specific subscription request parameters. Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. - apple: Always targets App Store - google: Targets Play Store by default, Horizon when built with horizon flavor, or Fire OS when built with amazon flavor (determined at build time, not runtime) class RequestSubscriptionPropsByPlatforms: ## Apple-specific subscription parameters var apple: RequestSubscriptionIosProps @@ -4313,6 +4314,34 @@ class RequestSubscriptionPropsByPlatforms: dict["android"] = android return dict +class RequestVerifyPurchaseWithIapkitAmazonProps: + ## Amazon Appstore user id returned by PurchaseResponse.getUserData().getUserId(). + var user_id: Variant = null + ## Amazon Appstore receipt id returned by PurchaseResponse.getReceipt().getReceiptId(). + var receipt_id: String = "" + ## Use Amazon RVS Cloud Sandbox for App Tester receipts. + var sandbox: Variant = null + + static func from_dict(data: Dictionary) -> RequestVerifyPurchaseWithIapkitAmazonProps: + var obj = RequestVerifyPurchaseWithIapkitAmazonProps.new() + if data.has("userId") and data["userId"] != null: + obj.user_id = data["userId"] + if data.has("receiptId") and data["receiptId"] != null: + obj.receipt_id = data["receiptId"] + if data.has("sandbox") and data["sandbox"] != null: + obj.sandbox = data["sandbox"] + return obj + + func to_dict() -> Dictionary: + var dict = {} + if user_id != null: + dict["userId"] = user_id + if receipt_id != null: + dict["receiptId"] = receipt_id + if sandbox != null: + dict["sandbox"] = sandbox + return dict + class RequestVerifyPurchaseWithIapkitAppleProps: ## The JWS token returned with the purchase response. var jws: String = "" @@ -4345,7 +4374,7 @@ class RequestVerifyPurchaseWithIapkitGoogleProps: dict["purchaseToken"] = purchase_token return dict -## Platform-specific verification parameters for IAPKit. - apple: Verifies via App Store (JWS token) - google: Verifies via Play Store (purchase token) +## Platform-specific verification parameters for IAPKit. - apple: Verifies via App Store (JWS token) - google: Verifies via Play Store (purchase token) - amazon: Verifies via Amazon Appstore RVS (userId + receiptId) class RequestVerifyPurchaseWithIapkitProps: ## API key used for the Authorization header (Bearer {apiKey}). var api_key: Variant = null @@ -4353,6 +4382,8 @@ class RequestVerifyPurchaseWithIapkitProps: var apple: RequestVerifyPurchaseWithIapkitAppleProps ## Google Play Store verification parameters. var google: RequestVerifyPurchaseWithIapkitGoogleProps + ## Amazon Appstore verification parameters. + var amazon: RequestVerifyPurchaseWithIapkitAmazonProps static func from_dict(data: Dictionary) -> RequestVerifyPurchaseWithIapkitProps: var obj = RequestVerifyPurchaseWithIapkitProps.new() @@ -4368,6 +4399,11 @@ class RequestVerifyPurchaseWithIapkitProps: obj.google = RequestVerifyPurchaseWithIapkitGoogleProps.from_dict(data["google"]) else: obj.google = data["google"] + if data.has("amazon") and data["amazon"] != null: + if data["amazon"] is Dictionary: + obj.amazon = RequestVerifyPurchaseWithIapkitAmazonProps.from_dict(data["amazon"]) + else: + obj.amazon = data["amazon"] return obj func to_dict() -> Dictionary: @@ -4384,6 +4420,11 @@ class RequestVerifyPurchaseWithIapkitProps: dict["google"] = google.to_dict() else: dict["google"] = google + if amazon != null: + if amazon.has_method("to_dict"): + dict["amazon"] = amazon.to_dict() + else: + dict["amazon"] = amazon return dict ## Product-level subscription replacement parameters (Android) Used with setSubscriptionProductReplacementParams in BillingFlowParams.ProductDetailsParams Available in Google Play Billing Library 8.1.0+ @@ -4728,7 +4769,8 @@ const IAP_STORE_VALUES = { IapStore.UNKNOWN: "unknown", IapStore.APPLE: "apple", IapStore.GOOGLE: "google", - IapStore.HORIZON: "horizon" + IapStore.HORIZON: "horizon", + IapStore.AMAZON: "amazon" } const PAYMENT_MODE_VALUES = { @@ -4997,7 +5039,8 @@ const IAP_STORE_FROM_STRING = { "unknown": IapStore.UNKNOWN, "apple": IapStore.APPLE, "google": IapStore.GOOGLE, - "horizon": IapStore.HORIZON + "horizon": IapStore.HORIZON, + "amazon": IapStore.AMAZON } const PAYMENT_MODE_FROM_STRING = { diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts index 518f5ffd..2e3ddc63 100644 --- a/packages/gql/src/generated/types.ts +++ b/packages/gql/src/generated/types.ts @@ -511,7 +511,7 @@ export type IapEvent = 'purchase-updated' | 'purchase-error' | 'promoted-product export type IapPlatform = 'ios' | 'android'; -export type IapStore = 'unknown' | 'apple' | 'google' | 'horizon'; +export type IapStore = 'unknown' | 'apple' | 'google' | 'horizon' | 'amazon'; /** Unified purchase states from IAPKit verification response. */ export type IapkitPurchaseState = 'entitled' | 'pending-acknowledgment' | 'pending' | 'canceled' | 'expired' | 'ready-to-consume' | 'consumed' | 'unknown' | 'inauthentic'; @@ -1585,7 +1585,8 @@ export type RequestPurchaseProps = * * Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. * - apple: Always targets App Store - * - google: Targets Play Store by default, or Horizon when built with horizon flavor + * - google: Targets Play Store by default, Horizon when built with horizon flavor, + * or Fire OS when built with amazon flavor * (determined at build time, not runtime) */ export interface RequestPurchasePropsByPlatforms { @@ -1679,7 +1680,8 @@ export interface RequestSubscriptionIosProps { * * Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. * - apple: Always targets App Store - * - google: Targets Play Store by default, or Horizon when built with horizon flavor + * - google: Targets Play Store by default, Horizon when built with horizon flavor, + * or Fire OS when built with amazon flavor * (determined at build time, not runtime) */ export interface RequestSubscriptionPropsByPlatforms { @@ -1693,6 +1695,15 @@ export interface RequestSubscriptionPropsByPlatforms { ios?: (RequestSubscriptionIosProps | null); } +export interface RequestVerifyPurchaseWithIapkitAmazonProps { + /** Amazon Appstore receipt id returned by PurchaseResponse.getReceipt().getReceiptId(). */ + receiptId: string; + /** Use Amazon RVS Cloud Sandbox for App Tester receipts. */ + sandbox?: (boolean | null); + /** Amazon Appstore user id returned by PurchaseResponse.getUserData().getUserId(). */ + userId?: (string | null); +} + export interface RequestVerifyPurchaseWithIapkitAppleProps { /** The JWS token returned with the purchase response. */ jws: string; @@ -1708,8 +1719,11 @@ export interface RequestVerifyPurchaseWithIapkitGoogleProps { * * - apple: Verifies via App Store (JWS token) * - google: Verifies via Play Store (purchase token) + * - amazon: Verifies via Amazon Appstore RVS (userId + receiptId) */ export interface RequestVerifyPurchaseWithIapkitProps { + /** Amazon Appstore verification parameters. */ + amazon?: (RequestVerifyPurchaseWithIapkitAmazonProps | null); /** API key used for the Authorization header (Bearer {apiKey}). */ apiKey?: (string | null); /** Apple App Store verification parameters. */ diff --git a/packages/gql/src/type.graphql b/packages/gql/src/type.graphql index 1bc2f085..e7f9cb59 100644 --- a/packages/gql/src/type.graphql +++ b/packages/gql/src/type.graphql @@ -49,6 +49,7 @@ enum IapStore { Apple Google Horizon + Amazon } # Common product fields @@ -219,7 +220,8 @@ Platform-specific purchase request parameters. Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. - apple: Always targets App Store -- google: Targets Play Store by default, or Horizon when built with horizon flavor +- google: Targets Play Store by default, Horizon when built with horizon flavor, + or Fire OS when built with amazon flavor (determined at build time, not runtime) """ input RequestPurchasePropsByPlatforms { @@ -246,7 +248,8 @@ Platform-specific subscription request parameters. Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. - apple: Always targets App Store -- google: Targets Play Store by default, or Horizon when built with horizon flavor +- google: Targets Play Store by default, Horizon when built with horizon flavor, + or Fire OS when built with amazon flavor (determined at build time, not runtime) """ input RequestSubscriptionPropsByPlatforms { @@ -292,7 +295,7 @@ input VerifyPurchaseProps { } union VerifyPurchaseResult = - VerifyPurchaseResultAndroid + | VerifyPurchaseResultAndroid | VerifyPurchaseResultIOS | VerifyPurchaseResultHorizon @@ -310,11 +313,27 @@ input RequestVerifyPurchaseWithIapkitGoogleProps { purchaseToken: String! } +input RequestVerifyPurchaseWithIapkitAmazonProps { + """ + Amazon Appstore user id returned by PurchaseResponse.getUserData().getUserId(). + """ + userId: String + """ + Amazon Appstore receipt id returned by PurchaseResponse.getReceipt().getReceiptId(). + """ + receiptId: String! + """ + Use Amazon RVS Cloud Sandbox for App Tester receipts. + """ + sandbox: Boolean +} + """ Platform-specific verification parameters for IAPKit. - apple: Verifies via App Store (JWS token) - google: Verifies via Play Store (purchase token) +- amazon: Verifies via Amazon Appstore RVS (userId + receiptId) """ input RequestVerifyPurchaseWithIapkitProps { """ @@ -329,6 +348,10 @@ input RequestVerifyPurchaseWithIapkitProps { Google Play Store verification parameters. """ google: RequestVerifyPurchaseWithIapkitGoogleProps + """ + Amazon Appstore verification parameters. + """ + amazon: RequestVerifyPurchaseWithIapkitAmazonProps } """ diff --git a/packages/kit/convex/_generated/api.d.ts b/packages/kit/convex/_generated/api.d.ts index cf45d8fb..f8fa0276 100644 --- a/packages/kit/convex/_generated/api.d.ts +++ b/packages/kit/convex/_generated/api.d.ts @@ -41,6 +41,7 @@ import type * as projects_mutation from "../projects/mutation.js"; import type * as projects_query from "../projects/query.js"; import type * as projects_setupStatus from "../projects/setupStatus.js"; import type * as purchases_action from "../purchases/action.js"; +import type * as purchases_amazon from "../purchases/amazon.js"; import type * as purchases_android from "../purchases/android.js"; import type * as purchases_cleanup from "../purchases/cleanup.js"; import type * as purchases_errors from "../purchases/errors.js"; @@ -121,6 +122,7 @@ declare const fullApi: ApiFromModules<{ "projects/query": typeof projects_query; "projects/setupStatus": typeof projects_setupStatus; "purchases/action": typeof purchases_action; + "purchases/amazon": typeof purchases_amazon; "purchases/android": typeof purchases_android; "purchases/cleanup": typeof purchases_cleanup; "purchases/errors": typeof purchases_errors; diff --git a/packages/kit/convex/analytics/action.ts b/packages/kit/convex/analytics/action.ts index 09064390..44fef10f 100644 --- a/packages/kit/convex/analytics/action.ts +++ b/packages/kit/convex/analytics/action.ts @@ -38,6 +38,7 @@ export const trackFirstReceiptVerified = internalAction({ v.literal("apple"), v.literal("google"), v.literal("horizon"), + v.literal("amazon"), ), }, handler: async (_ctx, args) => { diff --git a/packages/kit/convex/projects/mutation.ts b/packages/kit/convex/projects/mutation.ts index f370b19a..9a7c173a 100644 --- a/packages/kit/convex/projects/mutation.ts +++ b/packages/kit/convex/projects/mutation.ts @@ -155,6 +155,27 @@ function normalizeHorizonAppSecret(input: string): string { return normalized; } +function normalizeAmazonSharedSecret(input: string): string { + const normalized = input.trim(); + if (!normalized) { + throw createError( + ErrorCode.INVALID_INPUT, + "Amazon RVS shared secret cannot be empty.", + ); + } + // Amazon shared secrets are opaque strings copied from the Amazon + // Developer Console and may contain punctuation such as ':' and '='. + // Validate only a sane size envelope so we don't reject legitimate + // production secrets or sandbox placeholders. + if (normalized.length > 2_048) { + throw createError( + ErrorCode.INVALID_INPUT, + "Amazon RVS shared secret looks malformed (expected 1–2048 characters).", + ); + } + return normalized; +} + // Helper to generate URL-friendly slug function generateSlug(name: string): string { return name @@ -280,6 +301,7 @@ export const updateProject = mutation({ horizonEnabled: v.optional(v.boolean()), horizonAppId: v.optional(v.string()), horizonAppSecret: v.optional(v.string()), + amazonSharedSecret: v.optional(v.string()), reportingCurrency: v.optional(v.string()), }, handler: async (ctx, args) => { @@ -370,6 +392,11 @@ export const updateProject = mutation({ args.horizonAppSecret, ); } + if (args.amazonSharedSecret !== undefined) { + updates.amazonSharedSecret = normalizeAmazonSharedSecret( + args.amazonSharedSecret, + ); + } // Invariant: enabling Horizon without both credentials leaves the // project in a state where verify calls would throw diff --git a/packages/kit/convex/projects/query.ts b/packages/kit/convex/projects/query.ts index 81df17d4..56005beb 100644 --- a/packages/kit/convex/projects/query.ts +++ b/packages/kit/convex/projects/query.ts @@ -10,23 +10,27 @@ import { function projectWithSecretState(project: Doc<"projects">): Omit< Doc<"projects">, - "horizonAppSecret" + "horizonAppSecret" | "amazonSharedSecret" > & { hasHorizonAppSecret: boolean; + hasAmazonSharedSecret: boolean; } { - const { horizonAppSecret, ...rest } = project; + const { horizonAppSecret, amazonSharedSecret, ...rest } = project; return { ...rest, hasHorizonAppSecret: typeof horizonAppSecret === "string" && horizonAppSecret.length > 0, + hasAmazonSharedSecret: + typeof amazonSharedSecret === "string" && amazonSharedSecret.length > 0, }; } function projectForApiKeyLookup(project: Doc<"projects">): Omit< Doc<"projects">, - "apiKey" | "horizonAppSecret" + "apiKey" | "horizonAppSecret" | "amazonSharedSecret" > & { hasHorizonAppSecret: boolean; + hasAmazonSharedSecret: boolean; } { const { apiKey, ...rest } = projectWithSecretState(project); void apiKey; @@ -35,9 +39,10 @@ function projectForApiKeyLookup(project: Doc<"projects">): Omit< function projectForDashboard(project: Doc<"projects">): Omit< Doc<"projects">, - "apiKey" | "horizonAppSecret" + "apiKey" | "horizonAppSecret" | "amazonSharedSecret" > & { hasHorizonAppSecret: boolean; + hasAmazonSharedSecret: boolean; } { return projectForApiKeyLookup(project); } @@ -46,9 +51,13 @@ function projectForList( project: Doc<"projects">, projectIdsWithAnyKey: Set, projectIdsWithActiveKey: Set, -): Omit, "apiKey" | "horizonAppSecret"> & { +): Omit< + Doc<"projects">, + "apiKey" | "horizonAppSecret" | "amazonSharedSecret" +> & { hasApiKey: boolean; hasHorizonAppSecret: boolean; + hasAmazonSharedSecret: boolean; } { const { apiKey, ...rest } = projectWithSecretState(project); return { diff --git a/packages/kit/convex/projects/setupStatus.ts b/packages/kit/convex/projects/setupStatus.ts index 6334f817..4bcd9a55 100644 --- a/packages/kit/convex/projects/setupStatus.ts +++ b/packages/kit/convex/projects/setupStatus.ts @@ -30,6 +30,7 @@ export const getSetupStatus = query({ ios: platformShape, android: platformShape, horizon: platformShape, + amazon: platformShape, appleP8Uploaded: v.boolean(), googleServiceAccountUploaded: v.boolean(), }), @@ -49,6 +50,7 @@ export const getSetupStatus = query({ ios: empty, android: empty, horizon: empty, + amazon: empty, appleP8Uploaded: false, googleServiceAccountUploaded: false, }; @@ -78,6 +80,11 @@ export const getSetupStatus = query({ if (!project.horizonAppId) horizonMissing.push("horizonAppId"); if (!project.horizonAppSecret) horizonMissing.push("horizonAppSecret"); + const amazonMissing: string[] = []; + if (!project.amazonSharedSecret) { + amazonMissing.push("amazonSharedSecret"); + } + return { found: true, projectId: project._id, @@ -93,6 +100,10 @@ export const getSetupStatus = query({ configured: horizonMissing.length === 0, missing: horizonMissing, }, + amazon: { + configured: amazonMissing.length === 0, + missing: amazonMissing, + }, // The webhook receivers ALSO need the .p8 / service-account JSON // file uploaded to the project; check the `files` table directly // so the setup card reflects what the operator has actually diff --git a/packages/kit/convex/purchases/action.ts b/packages/kit/convex/purchases/action.ts index e8718367..b94860ed 100644 --- a/packages/kit/convex/purchases/action.ts +++ b/packages/kit/convex/purchases/action.ts @@ -1,7 +1,9 @@ "use node"; import { verifyGooglePlayReceiptInternalV1 } from "./android"; +import { verifyAmazonReceiptInternalV1 } from "./amazon"; import { verifyAppStoreReceiptInternalV1 } from "./ios"; export const verifyGooglePlayReceipt = verifyGooglePlayReceiptInternalV1; export const verifyAppStoreReceipt = verifyAppStoreReceiptInternalV1; +export const verifyAmazonReceipt = verifyAmazonReceiptInternalV1; diff --git a/packages/kit/convex/purchases/amazon.test.ts b/packages/kit/convex/purchases/amazon.test.ts new file mode 100644 index 00000000..0d15d305 --- /dev/null +++ b/packages/kit/convex/purchases/amazon.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, test, vi } from "vitest"; + +import { + buildAmazonRemoteId, + mapAmazonReceiptState, + parseAmazonReceiptResponse, +} from "./amazon"; +import { AmazonReceiptVerificationError } from "./errors"; +import { HarmonizedPurchaseState } from "./purchaseState"; + +describe("buildAmazonRemoteId", () => { + test("separates sandbox and production receipts", () => { + expect( + buildAmazonRemoteId({ + userId: "user/one", + receiptId: "receipt:one", + sandbox: true, + }), + ).toBe("sandbox:user%2Fone:receipt%3Aone"); + + expect( + buildAmazonRemoteId({ + userId: "user/one", + receiptId: "receipt:one", + sandbox: false, + }), + ).toBe("production:user%2Fone:receipt%3Aone"); + }); +}); + +describe("mapAmazonReceiptState", () => { + test("maps canceled receipts before product type handling", () => { + expect( + mapAmazonReceiptState({ + cancelDate: 1_700_000_000_000, + productType: "CONSUMABLE", + }), + ).toBe(HarmonizedPurchaseState.CANCELED); + }); + + test("maps Amazon product types to harmonized states", () => { + expect(mapAmazonReceiptState({ productType: "CONSUMABLE" })).toBe( + HarmonizedPurchaseState.READY_TO_CONSUME, + ); + expect(mapAmazonReceiptState({ productType: "ENTITLED" })).toBe( + HarmonizedPurchaseState.ENTITLED, + ); + expect(mapAmazonReceiptState({ productType: "SUBSCRIPTION" })).toBe( + HarmonizedPurchaseState.ENTITLED, + ); + }); + + test("maps expired subscription renewalDate to expired", () => { + vi.spyOn(Date, "now").mockReturnValue(2_000); + + expect( + mapAmazonReceiptState({ + productType: "SUBSCRIPTION", + renewalDate: 1_000, + }), + ).toBe(HarmonizedPurchaseState.EXPIRED); + + vi.restoreAllMocks(); + }); + + test("falls back to unknown for unrecognized product types", () => { + expect(mapAmazonReceiptState({ productType: "FUTURE_KIND" })).toBe( + HarmonizedPurchaseState.UNKNOWN, + ); + }); +}); + +describe("parseAmazonReceiptResponse", () => { + test("accepts object responses from RVS", () => { + const raw = { + productId: "dev.hyo.martie.premium", + productType: "SUBSCRIPTION", + }; + + expect(parseAmazonReceiptResponse(raw)).toBe(raw); + }); + + test("rejects non-object RVS responses", () => { + expect(() => parseAmazonReceiptResponse(null)).toThrow( + AmazonReceiptVerificationError, + ); + expect(() => parseAmazonReceiptResponse("not-json")).toThrow( + AmazonReceiptVerificationError, + ); + }); +}); diff --git a/packages/kit/convex/purchases/amazon.ts b/packages/kit/convex/purchases/amazon.ts new file mode 100644 index 00000000..7bca4afa --- /dev/null +++ b/packages/kit/convex/purchases/amazon.ts @@ -0,0 +1,267 @@ +"use node"; + +import { v } from "convex/values"; + +import { internal } from "../_generated/api"; +import { action } from "../_generated/server"; +import { + AmazonReceiptInvalidError, + AmazonReceiptVerificationError, + AmazonSharedSecretNotConfiguredError, + ReceiptVerificationError, +} from "./errors"; +import { HarmonizedPurchaseState } from "./purchaseState"; +import { + extractHttpStatus, + isTransientHttpError, + retryOnTransient, +} from "./retry"; +import { + getProjectByApiKey, + isValidState, + receiptResponseValidator, +} from "./shared"; + +const AMAZON_RVS_BASE_URL = "https://appstore-sdk.amazon.com"; +const AMAZON_RVS_VERSION = "1.0"; +const AMAZON_SANDBOX_SHARED_SECRET = "iapkit-sandbox"; + +export interface AmazonReceiptData { + autoRenewing?: boolean; + cancelDate?: number | null; + cancelReason?: number | null; + gracePeriodEndDate?: number | null; + productId?: string; + productType?: string; + purchaseDate?: number; + quantity?: number | null; + receiptId?: string; + renewalDate?: number | null; + term?: string | null; + termSku?: string | null; + testTransaction?: boolean; +} + +function describeError(error: unknown): string { + const status = (error as { code?: unknown })?.code; + const type = error instanceof Error ? error.name : typeof error; + return typeof status === "number" ? `${type} ${status}` : type; +} + +function encodePathSegment(value: string): string { + return encodeURIComponent(value); +} + +function buildAmazonRvsUrl(args: { + sharedSecret: string; + userId: string; + receiptId: string; + sandbox: boolean; +}): string { + const sandboxSegment = args.sandbox ? "/sandbox" : ""; + return ( + `${AMAZON_RVS_BASE_URL}${sandboxSegment}/version/${AMAZON_RVS_VERSION}` + + `/verifyReceiptId/developer/${encodePathSegment(args.sharedSecret)}` + + `/user/${encodePathSegment(args.userId)}` + + `/receiptId/${encodePathSegment(args.receiptId)}` + ); +} + +export function buildAmazonRemoteId(args: { + userId: string; + receiptId: string; + sandbox: boolean; +}): string { + return [ + args.sandbox ? "sandbox" : "production", + encodePathSegment(args.userId), + encodePathSegment(args.receiptId), + ].join(":"); +} + +export function mapAmazonReceiptState( + receipt: AmazonReceiptData, +): HarmonizedPurchaseState { + if (receipt.cancelDate !== undefined && receipt.cancelDate !== null) { + return HarmonizedPurchaseState.CANCELED; + } + + const productType = receipt.productType?.toUpperCase(); + switch (productType) { + case "CONSUMABLE": + return HarmonizedPurchaseState.READY_TO_CONSUME; + case "ENTITLED": + return HarmonizedPurchaseState.ENTITLED; + case "SUBSCRIPTION": + if ( + receipt.renewalDate !== undefined && + receipt.renewalDate !== null && + receipt.renewalDate < Date.now() + ) { + return HarmonizedPurchaseState.EXPIRED; + } + return HarmonizedPurchaseState.ENTITLED; + default: + return HarmonizedPurchaseState.UNKNOWN; + } +} + +export function parseAmazonReceiptResponse(raw: unknown): AmazonReceiptData { + if (!raw || typeof raw !== "object") { + throw new AmazonReceiptVerificationError( + "Amazon RVS returned an unparseable body.", + ); + } + + return raw; +} + +export const verifyAmazonReceiptInternalV1 = action({ + args: { + apiKey: v.string(), + userId: v.string(), + receiptId: v.string(), + sandbox: v.optional(v.boolean()), + requestIp: v.optional(v.string()), + }, + returns: receiptResponseValidator, + handler: async (ctx, args) => { + const verificationStart = Date.now(); + const project = await getProjectByApiKey(ctx, args.apiKey); + const sandbox = args.sandbox === true; + const sharedSecret = project.amazonSharedSecret?.trim(); + + if (!sandbox && !sharedSecret) { + throw new AmazonSharedSecretNotConfiguredError(); + } + + const requestData = { + store: "amazon" as const, + userId: args.userId, + receiptId: args.receiptId, + sandbox, + }; + const applicationId = project.androidPackageName ?? "amazon-appstore"; + const remoteId = buildAmazonRemoteId({ + userId: args.userId, + receiptId: args.receiptId, + sandbox, + }); + const url = buildAmazonRvsUrl({ + sharedSecret: sharedSecret || AMAZON_SANDBOX_SHARED_SECRET, + userId: args.userId, + receiptId: args.receiptId, + sandbox, + }); + + let parsedBody: unknown; + try { + parsedBody = await retryOnTransient( + async () => { + const res = await fetch(url, { + method: "GET", + headers: { Accept: "application/json" }, + }); + const bodyText = await res.text().catch(() => ""); + + if (res.status === 400 || res.status === 497) { + throw new AmazonReceiptInvalidError( + res.status, + bodyText.slice(0, 512) || + (res.status === 497 ? "invalid user ID" : "invalid receipt"), + ); + } + if (res.status === 410) { + throw new AmazonReceiptInvalidError( + res.status, + bodyText.slice(0, 512) || "receipt is no longer valid", + ); + } + if (res.status === 496) { + throw new AmazonReceiptVerificationError("invalid shared secret"); + } + if (!res.ok) { + const err = new Error( + `Amazon RVS ${res.status}: ${bodyText.slice(0, 512)}`, + ); + (err as { code?: number }).code = res.status; + throw err; + } + + return bodyText ? (JSON.parse(bodyText) as unknown) : {}; + }, + { + shouldRetry: (error) => + extractHttpStatus(error) === 429 || isTransientHttpError(error), + }, + ); + } catch (error) { + if (error instanceof AmazonReceiptInvalidError) { + const state = + error.errorDetails?.status === 410 + ? HarmonizedPurchaseState.CANCELED + : HarmonizedPurchaseState.INAUTHENTIC; + await ctx.runMutation(internal.purchases.internal.saveReceiptInternal, { + projectId: project._id, + store: "amazon", + applicationId, + remoteId, + requestData, + remoteResponse: JSON.stringify({ + error: error.errorCode, + message: error.errorMessage, + details: error.errorDetails ?? null, + }), + state, + isValid: false, + requestIp: args.requestIp, + verificationDurationMs: Date.now() - verificationStart, + }); + return { isValid: false, state }; + } + + const message = describeError(error); + await ctx.runMutation(internal.purchases.internal.saveReceiptInternal, { + projectId: project._id, + store: "amazon", + applicationId, + remoteId, + requestData, + remoteResponse: JSON.stringify({ + error: "AMAZON_RECEIPT_VERIFICATION_ERROR", + message, + }), + state: HarmonizedPurchaseState.UNKNOWN, + isValid: false, + requestIp: args.requestIp, + verificationDurationMs: Date.now() - verificationStart, + }); + throw new AmazonReceiptVerificationError(message); + } + + const receiptData = parseAmazonReceiptResponse(parsedBody); + const state = mapAmazonReceiptState(receiptData); + const remoteResponse = JSON.stringify(receiptData); + + await ctx.runMutation(internal.purchases.internal.saveReceiptInternal, { + projectId: project._id, + store: "amazon", + applicationId, + remoteId, + requestData, + remoteResponse, + state, + isValid: isValidState(state), + requestIp: args.requestIp, + verificationDurationMs: Date.now() - verificationStart, + }); + + return { + isValid: isValidState(state), + state, + ...(receiptData.productId ? { productId: receiptData.productId } : {}), + }; + }, +}); + +export { ReceiptVerificationError }; diff --git a/packages/kit/convex/purchases/errors.ts b/packages/kit/convex/purchases/errors.ts index 594c5d14..4fdcb86d 100644 --- a/packages/kit/convex/purchases/errors.ts +++ b/packages/kit/convex/purchases/errors.ts @@ -66,6 +66,35 @@ export class MetaHorizonVerificationError extends ReceiptVerificationError { } } +export class AmazonSharedSecretNotConfiguredError extends ReceiptVerificationError { + constructor() { + super( + "AMAZON_SHARED_SECRET_NOT_CONFIGURED", + "Amazon RVS shared secret is not set for this project. Configure it in project settings before verifying production Amazon receipts.", + ); + } +} + +export class AmazonReceiptInvalidError extends ReceiptVerificationError { + constructor(status: number, detail: string) { + super( + "AMAZON_RECEIPT_INVALID", + `Amazon RVS rejected the receipt: ${detail}`, + { status }, + ); + } +} + +export class AmazonReceiptVerificationError extends ReceiptVerificationError { + constructor(detail: string) { + super( + "AMAZON_RECEIPT_VERIFICATION_ERROR", + `Amazon RVS verification failed: ${detail}`, + { originalError: detail }, + ); + } +} + export class PlayStoreServiceAccountNotFoundError extends ReceiptVerificationError { constructor() { super( diff --git a/packages/kit/convex/purchases/extract-product-id.test.ts b/packages/kit/convex/purchases/extract-product-id.test.ts index ad7aca33..3202cb5e 100644 --- a/packages/kit/convex/purchases/extract-product-id.test.ts +++ b/packages/kit/convex/purchases/extract-product-id.test.ts @@ -38,6 +38,35 @@ describe("extractProductIdFromRemoteResponse (horizon)", () => { }); }); +describe("extractProductIdFromRemoteResponse (amazon)", () => { + test("returns the Amazon RVS productId from the persisted payload", () => { + const remote = JSON.stringify({ + productId: "dev.hyo.martie.premium", + productType: "SUBSCRIPTION", + receiptId: "receipt-123", + }); + + expect(extractProductIdFromRemoteResponse("amazon", remote)).toBe( + "dev.hyo.martie.premium", + ); + }); + + test("returns null when the Amazon productId is missing or wrong type", () => { + expect( + extractProductIdFromRemoteResponse( + "amazon", + JSON.stringify({ receiptId: "receipt-123" }), + ), + ).toBeNull(); + expect( + extractProductIdFromRemoteResponse( + "amazon", + JSON.stringify({ productId: 42 }), + ), + ).toBeNull(); + }); +}); + describe("extractProductIdFromRemoteResponse", () => { it("returns null when remoteResponse is missing", () => { expect(extractProductIdFromRemoteResponse("apple", undefined)).toBeNull(); diff --git a/packages/kit/convex/purchases/shared.ts b/packages/kit/convex/purchases/shared.ts index 9289f47e..cd1d67b2 100644 --- a/packages/kit/convex/purchases/shared.ts +++ b/packages/kit/convex/purchases/shared.ts @@ -445,7 +445,7 @@ export function isValidState(state: HarmonizedPurchaseState): boolean { * so no node-only import is introduced. */ export function extractOrderIdFromRemoteResponse( - store: "apple" | "google" | "horizon", + store: "apple" | "google" | "horizon" | "amazon", remoteResponse?: string | null, ): string | null { if (store !== "google" || !remoteResponse) { @@ -515,7 +515,7 @@ export function extractOrderIdFromRemoteResponse( * importing from a query module. */ export function extractProductIdFromRemoteResponse( - store: "apple" | "google" | "horizon", + store: "apple" | "google" | "horizon" | "amazon", remoteResponse?: string | null, ): string | null { if (!remoteResponse) { @@ -575,6 +575,14 @@ export function extractProductIdFromRemoteResponse( ) { return (parsed as { sku: string }).sku; } + + if ( + store === "amazon" && + "productId" in parsed && + typeof (parsed as { productId?: unknown }).productId === "string" + ) { + return (parsed as { productId: string }).productId; + } } } catch { return null; diff --git a/packages/kit/convex/schema.ts b/packages/kit/convex/schema.ts index 625711f0..c198ba60 100644 --- a/packages/kit/convex/schema.ts +++ b/packages/kit/convex/schema.ts @@ -23,6 +23,7 @@ export const purchaseStoreValidator = v.union( v.literal("apple"), v.literal("google"), v.literal("horizon"), + v.literal("amazon"), ); export const purchaseRequestDataValidator = v.union( @@ -46,6 +47,16 @@ export const purchaseRequestDataValidator = v.union( userId: v.string(), sku: v.string(), }), + // Amazon Appstore SDK IAP: RVS verifies a server-held shared secret + // with the userId + receiptId returned by Amazon's purchase APIs. + // Sandbox receipts are generated by App Tester and use the same + // shape with `sandbox: true`. + v.object({ + store: v.literal("amazon"), + userId: v.string(), + receiptId: v.string(), + sandbox: v.optional(v.boolean()), + }), ); // The schema is the source of truth for the database structure. @@ -221,6 +232,12 @@ const schema = defineSchema({ horizonAppId: v.optional(v.union(v.string(), v.null())), horizonAppSecret: v.optional(v.union(v.string(), v.null())), + // Amazon Appstore Receipt Verification Service (RVS). Production + // calls require the developer shared secret; Cloud Sandbox accepts + // any non-empty shared secret but we keep one project-level field + // so clients don't ever ship the production secret. + amazonSharedSecret: v.optional(v.union(v.string(), v.null())), + // Stable presentation currency for dashboard analytics. Raw // purchases/subscriptions keep their original store currency; // IAPKit does not do FX conversion; reporting totals only include diff --git a/packages/kit/server/api/v1/replay-guard.test.ts b/packages/kit/server/api/v1/replay-guard.test.ts index f7f5ce69..38e1fd10 100644 --- a/packages/kit/server/api/v1/replay-guard.test.ts +++ b/packages/kit/server/api/v1/replay-guard.test.ts @@ -56,6 +56,30 @@ describe("hashPayload", () => { }); expect(left).not.toBe(right); }); + + test("distinguishes Amazon receipt tuples and sandbox mode", () => { + const tupleLeft = hashPayload({ + store: "amazon", + userId: "ab", + receiptId: "c", + sandbox: true, + }); + const tupleRight = hashPayload({ + store: "amazon", + userId: "a", + receiptId: "bc", + sandbox: true, + }); + const production = hashPayload({ + store: "amazon", + userId: "ab", + receiptId: "c", + sandbox: false, + }); + + expect(tupleLeft).not.toBe(tupleRight); + expect(tupleLeft).not.toBe(production); + }); }); describe("tryConsumeReplay", () => { diff --git a/packages/kit/server/api/v1/replay-guard.ts b/packages/kit/server/api/v1/replay-guard.ts index 3bc4ec06..d57e2ebc 100644 --- a/packages/kit/server/api/v1/replay-guard.ts +++ b/packages/kit/server/api/v1/replay-guard.ts @@ -35,9 +35,12 @@ export interface ReplayBucket { // returned `isValid: false`. Subsequent // requests for the exact same payload are short-circuited with // `REPEATED_FAILURE` until the cooldown expires — re-asking - // Apple / Google / Meta about a receipt they already rejected, or - // retrying the same failed product-match guard, has no chance of - // changing the answer in seconds. + // Apple / Google / Meta / Amazon about a receipt they already + // rejected, or retrying the same failed product-match guard, has + // no chance of changing the answer in seconds. An attacker + // replaying a captured-then-revoked receipt should hit a hard wall + // instead of being able to rotate timing under the per-request + // burst cap to keep burning upstream API quota. lastFailureMs?: number; } @@ -71,7 +74,8 @@ export function hashPayload( body: | { store: "apple"; jws: string; expectedProductId?: string } | { store: "google"; purchaseToken: string; expectedProductId?: string } - | { store: "horizon"; userId: string; sku: string }, + | { store: "horizon"; userId: string; sku: string } + | { store: "amazon"; userId: string; receiptId: string; sandbox?: boolean }, ): string { const hasher = crypto.createHash("sha256"); hasher.update(body.store); @@ -96,6 +100,13 @@ export function hashPayload( hasher.update("\0"); hasher.update(body.sku); break; + case "amazon": + hasher.update(body.userId); + hasher.update("\0"); + hasher.update(body.receiptId); + hasher.update("\0"); + hasher.update(body.sandbox === true ? "sandbox" : "production"); + break; } return hasher.digest("hex").slice(0, 16); } @@ -285,11 +296,17 @@ export function replayGuardMiddleware( } // Valid-by-schema by the time this runs — the valibot validator - // upstream guarantees one of the three discriminated shapes. + // upstream guarantees one of the discriminated shapes. const body = c.req.valid("json" as never) as | { store: "apple"; jws: string; expectedProductId?: string } | { store: "google"; purchaseToken: string; expectedProductId?: string } - | { store: "horizon"; userId: string; sku: string }; + | { store: "horizon"; userId: string; sku: string } + | { + store: "amazon"; + userId: string; + receiptId: string; + sandbox?: boolean; + }; const bucketKey = `${apiKeyHash}:${hashPayload(body)}`; const result = tryConsumeReplay( diff --git a/packages/kit/server/api/v1/request-logger.test.ts b/packages/kit/server/api/v1/request-logger.test.ts index 7fda3cdb..6e8622f1 100644 --- a/packages/kit/server/api/v1/request-logger.test.ts +++ b/packages/kit/server/api/v1/request-logger.test.ts @@ -19,6 +19,8 @@ const TEST_APPLE_JWS = `${"a".repeat(42)}.${"b".repeat(42)}.${"c".repeat(42)}`; const TEST_GOOGLE_TOKEN = "t".repeat(40); const TEST_HORIZON_USER_ID = "user_123"; const TEST_HORIZON_SKU = "premium.monthly"; +const TEST_AMAZON_USER_ID = "amzn1.account.ABC123"; +const TEST_AMAZON_RECEIPT_ID = "amzn1.receipt.ABC123456789"; type TestVars = { apiKey?: string; @@ -168,6 +170,29 @@ describe("requestLoggerMiddleware", () => { expect(logs[0].store).toBe("horizon"); }); + test("logs Amazon verification store values", async () => { + const logs: VerifyLogLine[] = []; + const app = buildApp({ logs }); + + const res = await app.request("/verify", { + method: "POST", + headers: { + Authorization: "Bearer key-amazon", + "content-type": "application/json", + }, + body: JSON.stringify({ + store: "amazon", + userId: TEST_AMAZON_USER_ID, + receiptId: TEST_AMAZON_RECEIPT_ID, + sandbox: true, + }), + }); + + expect(res.status).toBe(200); + expect(logs).toHaveLength(1); + expect(logs[0].store).toBe("amazon"); + }); + test("populates the X-Correlation-Id response header even on validator failure", async () => { const logs: VerifyLogLine[] = []; const app = buildApp({ logs }); diff --git a/packages/kit/server/api/v1/request-logger.ts b/packages/kit/server/api/v1/request-logger.ts index cd4d13e1..19539694 100644 --- a/packages/kit/server/api/v1/request-logger.ts +++ b/packages/kit/server/api/v1/request-logger.ts @@ -9,7 +9,7 @@ import { hashApiKey } from "./rate-limit"; // never log the plaintext API key — only the SHA-256 prefix the rate // limiter already uses — so log leaks don't become credential leaks. -export type VerifyStore = "apple" | "google" | "horizon"; +export type VerifyStore = "apple" | "google" | "horizon" | "amazon"; export interface VerifyOutcome { isValid: boolean; diff --git a/packages/kit/server/api/v1/route-input-schemas.test.ts b/packages/kit/server/api/v1/route-input-schemas.test.ts index a09eb88d..a4875c8d 100644 --- a/packages/kit/server/api/v1/route-input-schemas.test.ts +++ b/packages/kit/server/api/v1/route-input-schemas.test.ts @@ -12,6 +12,8 @@ function parse(input: unknown) { // a URL-safe blob ≥ 20 chars. const VALID_APPLE_JWS = `${"a".repeat(40)}.${"b".repeat(40)}.${"c".repeat(40)}`; const VALID_GOOGLE_TOKEN = "a".repeat(40); +const VALID_AMAZON_USER_ID = "amzn1.account.ABC123"; +const VALID_AMAZON_RECEIPT_ID = "amzn1.receipt.ABC123456789=:1"; describe("verifyPurchaseInputSchema", () => { test("accepts a well-formed Apple payload", () => { @@ -78,6 +80,50 @@ describe("verifyPurchaseInputSchema", () => { expect(result.success).toBe(true); }); + test("accepts a well-formed Amazon payload", () => { + const result = parse({ + store: "amazon", + userId: VALID_AMAZON_USER_ID, + receiptId: VALID_AMAZON_RECEIPT_ID, + sandbox: true, + }); + expect(result.success).toBe(true); + }); + + test("rejects empty Amazon userId / receiptId", () => { + expect( + parse({ + store: "amazon", + userId: "", + receiptId: VALID_AMAZON_RECEIPT_ID, + }).success, + ).toBe(false); + expect( + parse({ + store: "amazon", + userId: VALID_AMAZON_USER_ID, + receiptId: "", + }).success, + ).toBe(false); + }); + + test("rejects Amazon payloads past their ceilings", () => { + expect( + parse({ + store: "amazon", + userId: "a".repeat(513), + receiptId: VALID_AMAZON_RECEIPT_ID, + }).success, + ).toBe(false); + expect( + parse({ + store: "amazon", + userId: VALID_AMAZON_USER_ID, + receiptId: "a".repeat(4_097), + }).success, + ).toBe(false); + }); + test("rejects empty Horizon userId / sku", () => { expect( parse({ store: "horizon", userId: "", sku: "coin_pack_100" }).success, @@ -182,4 +228,21 @@ describe("verifyPurchaseInputSchema", () => { }).success, ).toBe(false); }); + + test("rejects Amazon userId / receiptId with invalid characters", () => { + expect( + parse({ + store: "amazon", + userId: "bad user", + receiptId: VALID_AMAZON_RECEIPT_ID, + }).success, + ).toBe(false); + expect( + parse({ + store: "amazon", + userId: VALID_AMAZON_USER_ID, + receiptId: "", + }).success, + ).toBe(false); + }); }); diff --git a/packages/kit/server/api/v1/route-input-schemas.ts b/packages/kit/server/api/v1/route-input-schemas.ts index f7a64ad6..a51d936c 100644 --- a/packages/kit/server/api/v1/route-input-schemas.ts +++ b/packages/kit/server/api/v1/route-input-schemas.ts @@ -6,13 +6,16 @@ import * as v from "valibot"; // push the Bun server into an OOM by posting a multi-megabyte string. // Apple JWS transactions are typically ~1–2 KB; nested subscription // payloads stay well under 10 KB. Google purchase tokens are opaque -// base64 blobs, historically under ~200 chars. Product identifiers and -// Meta Horizon's (userId, sku) are short strings. +// base64 blobs, historically under ~200 chars. Product identifiers, +// Meta Horizon's (userId, sku), and Amazon RVS's (userId, receiptId) +// are short bounded strings. export const APPLE_JWS_MAX_LENGTH = 16_000; export const GOOGLE_PURCHASE_TOKEN_MAX_LENGTH = 2_000; const HORIZON_USER_ID_MAX_LENGTH = 256; const HORIZON_SKU_MAX_LENGTH = 256; const EXPECTED_PRODUCT_ID_MAX_LENGTH = 256; +const AMAZON_USER_ID_MAX_LENGTH = 512; +const AMAZON_RECEIPT_ID_MAX_LENGTH = 4_096; // Lower bounds — any real token from the respective store sits well // above these. A sub-threshold input is guaranteed garbage (empty @@ -24,6 +27,8 @@ const EXPECTED_PRODUCT_ID_MAX_LENGTH = 256; const APPLE_JWS_MIN_LENGTH = 100; const GOOGLE_PURCHASE_TOKEN_MIN_LENGTH = 20; const HORIZON_USER_ID_MIN_LENGTH = 3; +const AMAZON_USER_ID_MIN_LENGTH = 3; +const AMAZON_RECEIPT_ID_MIN_LENGTH = 10; // `sku` only enforces non-empty (via `v.nonEmpty` below); a 1-char // minimum would be redundant. Raise the floor here only if a real // shortest-known SKU justifies it. @@ -47,6 +52,8 @@ const GOOGLE_PURCHASE_TOKEN_PATTERN = /^[A-Za-z0-9._~-]+$/; const HORIZON_USER_ID_PATTERN = /^[A-Za-z0-9_-]+$/; const HORIZON_SKU_PATTERN = /^[A-Za-z0-9._-]+$/; const EXPECTED_PRODUCT_ID_PATTERN = /^[A-Za-z0-9._-]+$/; +const AMAZON_USER_ID_PATTERN = /^[A-Za-z0-9._~=-]+$/; +const AMAZON_RECEIPT_ID_PATTERN = /^[A-Za-z0-9._~:=/-]+$/; const expectedProductIdSchema = v.optional( v.pipe( @@ -155,4 +162,56 @@ export const verifyPurchaseInputSchema = v.variant("store", [ }), v.title("Meta Horizon (Quest)"), ), + v.pipe( + v.object({ + store: v.literal("amazon"), + userId: v.pipe( + v.string(), + v.nonEmpty("userId must not be empty."), + v.minLength( + AMAZON_USER_ID_MIN_LENGTH, + `userId must be at least ${AMAZON_USER_ID_MIN_LENGTH} characters.`, + ), + v.maxLength( + AMAZON_USER_ID_MAX_LENGTH, + `userId must be at most ${AMAZON_USER_ID_MAX_LENGTH} characters.`, + ), + v.regex( + AMAZON_USER_ID_PATTERN, + "userId must contain only URL-safe Amazon RVS characters.", + ), + v.description( + "Amazon user id returned by PurchaseResponse.getUserData().getUserId().", + ), + ), + receiptId: v.pipe( + v.string(), + v.nonEmpty("receiptId must not be empty."), + v.minLength( + AMAZON_RECEIPT_ID_MIN_LENGTH, + `receiptId must be at least ${AMAZON_RECEIPT_ID_MIN_LENGTH} characters.`, + ), + v.maxLength( + AMAZON_RECEIPT_ID_MAX_LENGTH, + `receiptId must be at most ${AMAZON_RECEIPT_ID_MAX_LENGTH} characters.`, + ), + v.regex( + AMAZON_RECEIPT_ID_PATTERN, + "receiptId must contain only URL-safe Amazon RVS characters.", + ), + v.description( + "Amazon receipt id returned by PurchaseResponse.getReceipt().getReceiptId() or PurchaseUpdatesResponse.getReceipts().", + ), + ), + sandbox: v.optional( + v.pipe( + v.boolean(), + v.description( + "Use Amazon RVS Cloud Sandbox for App Tester receipts.", + ), + ), + ), + }), + v.title("Amazon Appstore"), + ), ]); diff --git a/packages/kit/server/api/v1/routes.ts b/packages/kit/server/api/v1/routes.ts index 569ccecb..21022e7c 100644 --- a/packages/kit/server/api/v1/routes.ts +++ b/packages/kit/server/api/v1/routes.ts @@ -217,7 +217,9 @@ const verifyPurchaseRouteDescription = describeRoute({ ' • Google — `{ store: "google", purchaseToken, expectedProductId? }`\n' + ' • Horizon — `{ store: "horizon", userId, sku }` (Meta Quest;' + " IAPKit holds the App ID + App Secret and composes" + - " `OC|APP_ID|APP_SECRET` server-side)\n\n" + + " `OC|APP_ID|APP_SECRET` server-side)\n" + + ' • Amazon — `{ store: "amazon", userId, receiptId, sandbox? }`' + + " (Amazon Appstore SDK RVS; IAPKit holds the shared secret)\n\n" + "`expectedProductId` is optional for Apple / Google. When present, " + "IAPKit compares it against the product id verified by the upstream " + 'store and returns `isValid: false`, `state: "INAUTHENTIC"` on ' + @@ -236,7 +238,8 @@ const verifyPurchaseRouteDescription = describeRoute({ "headers.\n\n" + "Input size caps: request body ≤ 32 KB, `jws` ≤ 16 KB, " + "`purchaseToken` ≤ 2 KB, `expectedProductId` ≤ 256 chars, " + - "`userId` ≤ 256 chars, `sku` ≤ 256 chars. " + + "Meta `userId` ≤ 256 chars, `sku` ≤ 256 chars. " + + "Amazon `userId` ≤ 512 chars and `receiptId` ≤ 4 KB. " + "Oversized fields return `400 INVALID_INPUT`; oversized request " + "bodies return `413 PAYLOAD_TOO_LARGE`. Neither hits the upstream store.", security: [{ apiKey: [] }], @@ -334,7 +337,13 @@ const verifyPurchaseRouteDescription = describeRoute({ type VerifyPurchaseJson = | { store: "apple"; jws: string; expectedProductId?: string } | { store: "google"; purchaseToken: string; expectedProductId?: string } - | { store: "horizon"; userId: string; sku: string }; + | { store: "horizon"; userId: string; sku: string } + | { + store: "amazon"; + userId: string; + receiptId: string; + sandbox?: boolean; + }; // Tell Hono's Context what `c.req.valid("json")` returns for this // route so we don't need a `"json" as never` cast + `as VerifyPurchaseJson`. @@ -406,6 +415,21 @@ const verifyPurchaseHandler = async ( setOutcome({ isValid: horizon.isValid, state: horizon.state }); return c.json(horizon); } + case "amazon": { + const amazon = await client.action( + api.purchases.amazon.verifyAmazonReceiptInternalV1, + { + apiKey, + userId: json.userId, + receiptId: json.receiptId, + sandbox: json.sandbox, + requestIp, + }, + ); + + setOutcome({ isValid: amazon.isValid, state: amazon.state }); + return c.json(amazon); + } } } catch (error) { const convexError = handleConvexError(error); diff --git a/packages/kit/src/pages/auth/organization/project/PurchasesTable.tsx b/packages/kit/src/pages/auth/organization/project/PurchasesTable.tsx index 81ea44f0..0a279539 100644 --- a/packages/kit/src/pages/auth/organization/project/PurchasesTable.tsx +++ b/packages/kit/src/pages/auth/organization/project/PurchasesTable.tsx @@ -2,7 +2,15 @@ import { useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { Badge } from "@/components/Badge"; import { cn } from "@/lib/utils"; -import { Apple, CheckCircle2, Loader2, Play, XCircle } from "lucide-react"; +import { + Apple, + CheckCircle2, + Headset, + Loader2, + Play, + ShoppingBag, + XCircle, +} from "lucide-react"; import { formatReceiptDate, getPurchaseStateDisplay } from "./receipt-utils"; const storeIndicatorConfig = { @@ -14,6 +22,14 @@ const storeIndicatorConfig = { icon: Play, label: "Google Play", }, + horizon: { + icon: Headset, + label: "Meta Horizon", + }, + amazon: { + icon: ShoppingBag, + label: "Amazon Appstore", + }, } as const; type StoreIndicatorKey = keyof typeof storeIndicatorConfig; diff --git a/packages/kit/src/pages/auth/organization/project/analytics.tsx b/packages/kit/src/pages/auth/organization/project/analytics.tsx index b359a088..ff7c522d 100644 --- a/packages/kit/src/pages/auth/organization/project/analytics.tsx +++ b/packages/kit/src/pages/auth/organization/project/analytics.tsx @@ -30,7 +30,10 @@ import { api } from "@/convex"; import { PageLoading } from "@/components/LoadingSpinner"; import { cn, formatMicros, normalizeCurrencyCode } from "@/lib/utils"; -type DashboardProject = Omit, "apiKey" | "horizonAppSecret">; +type DashboardProject = Omit< + Doc<"projects">, + "apiKey" | "horizonAppSecret" | "amazonSharedSecret" +>; type ProjectContext = { project: DashboardProject }; type Platform = "IOS" | "Android"; diff --git a/packages/kit/src/pages/auth/organization/project/products.tsx b/packages/kit/src/pages/auth/organization/project/products.tsx index d2e2f9ce..50ef6a57 100644 --- a/packages/kit/src/pages/auth/organization/project/products.tsx +++ b/packages/kit/src/pages/auth/organization/project/products.tsx @@ -23,7 +23,10 @@ import { Tooltip } from "@/components/Tooltip"; import { Badge, PlatformBadge } from "../../../../components/Badge"; import { usdPriceToMicros } from "./productPrice"; -type DashboardProject = Omit, "apiKey" | "horizonAppSecret">; +type DashboardProject = Omit< + Doc<"projects">, + "apiKey" | "horizonAppSecret" | "amazonSharedSecret" +>; type ProjectContext = { project: DashboardProject }; type SyncJob = Doc<"productSyncJobs">; diff --git a/packages/kit/src/pages/auth/organization/project/purchase-detail.tsx b/packages/kit/src/pages/auth/organization/project/purchase-detail.tsx index c66a4b92..dc128450 100644 --- a/packages/kit/src/pages/auth/organization/project/purchase-detail.tsx +++ b/packages/kit/src/pages/auth/organization/project/purchase-detail.tsx @@ -126,6 +126,12 @@ export default function PurchaseDetail() { if (store === "google") { return "Google Play"; } + if (store === "horizon") { + return "Meta Horizon"; + } + if (store === "amazon") { + return "Amazon Appstore"; + } return store ?? FALLBACK_VALUE; }; diff --git a/packages/kit/src/pages/auth/organization/project/purchases.tsx b/packages/kit/src/pages/auth/organization/project/purchases.tsx index 57f2e6c2..274cc6ee 100644 --- a/packages/kit/src/pages/auth/organization/project/purchases.tsx +++ b/packages/kit/src/pages/auth/organization/project/purchases.tsx @@ -46,6 +46,7 @@ type PurchaseStats = { }; type CardKey = "total" | "apple" | "google" | "valid" | "invalid"; +type StoreFilter = "apple" | "google" | "horizon" | "amazon"; const STATS_LABELS: Record = { total: "Total Purchases", @@ -87,7 +88,12 @@ export default function ProjectPurchases() { const requestIpQuery = searchParams.get("ip") ?? ""; const storeParam = searchParams.get("store"); const storeFilter = - storeParam === "apple" || storeParam === "google" ? storeParam : undefined; + storeParam === "apple" || + storeParam === "google" || + storeParam === "horizon" || + storeParam === "amazon" + ? storeParam + : undefined; const sortFieldParam = searchParams.get("sortField") ?? "_creationTime"; const sortDirectionParam = searchParams.get("sortDirection") ?? "desc"; const sortField: "_creationTime" | "updatedAt" | "verificationDurationMs" = @@ -237,7 +243,7 @@ export default function ProjectPurchases() { setExclusiveFilters({}); }; - const applyStoreFilter = (store: "apple" | "google") => { + const applyStoreFilter = (store: StoreFilter) => { setExclusiveFilters({ store }); }; @@ -418,6 +424,14 @@ export default function ProjectPurchases() { value: "google", label: "Google Play", }, + { + value: "horizon", + label: "Meta Horizon", + }, + { + value: "amazon", + label: "Amazon Appstore", + }, ]} /> diff --git a/packages/kit/src/pages/auth/organization/project/settings.tsx b/packages/kit/src/pages/auth/organization/project/settings.tsx index d00111ba..79d2b381 100644 --- a/packages/kit/src/pages/auth/organization/project/settings.tsx +++ b/packages/kit/src/pages/auth/organization/project/settings.tsx @@ -50,6 +50,10 @@ interface ProjectData { // whether one is configured, so the UI can show "Configured / // Replace" instead of a prefilled password field. hasHorizonAppSecret?: boolean; + // Amazon RVS shared secret is also write-only. The dashboard only + // receives this boolean so the browser never sees the production + // secret after setup. + hasAmazonSharedSecret?: boolean; } interface OutletContext { @@ -115,9 +119,13 @@ export default function ProjectSettings() { const [horizonAppSecret, setHorizonAppSecret] = useState(""); const [isReplacingHorizonAppSecret, setIsReplacingHorizonAppSecret] = useState(false); + const [amazonSharedSecret, setAmazonSharedSecret] = useState(""); + const [isReplacingAmazonSharedSecret, setIsReplacingAmazonSharedSecret] = + useState(false); const [savingMetadata, setSavingMetadata] = useState(false); const [savingReportingCurrency, setSavingReportingCurrency] = useState(false); const [savingHorizon, setSavingHorizon] = useState(false); + const [savingAmazon, setSavingAmazon] = useState(false); // Convex mutations for file upload const generateUploadUrl = useMutation(api.files.mutation.generateUploadUrl); @@ -168,6 +176,9 @@ export default function ProjectSettings() { const originalHorizonEnabled = Boolean(project?.horizonEnabled); const originalHorizonAppId = project?.horizonAppId ?? ""; const hasHorizonAppSecretConfigured = Boolean(project?.hasHorizonAppSecret); + const hasAmazonSharedSecretConfigured = Boolean( + project?.hasAmazonSharedSecret, + ); useEffect(() => { if (!project) { @@ -187,6 +198,8 @@ export default function ProjectSettings() { // user-typed input silently being re-submitted. setHorizonAppSecret(""); setIsReplacingHorizonAppSecret(false); + setAmazonSharedSecret(""); + setIsReplacingAmazonSharedSecret(false); }, [ project, originalAndroidPackageName, @@ -199,6 +212,7 @@ export default function ProjectSettings() { originalHorizonEnabled, originalHorizonAppId, hasHorizonAppSecretConfigured, + hasAmazonSharedSecretConfigured, ]); const trimmedAndroidPackageName = androidPackageName.trim(); @@ -210,6 +224,7 @@ export default function ProjectSettings() { const trimmedReportingCurrency = reportingCurrency.trim().toUpperCase(); const trimmedHorizonAppId = horizonAppId.trim(); const trimmedHorizonAppSecret = horizonAppSecret.trim(); + const trimmedAmazonSharedSecret = amazonSharedSecret.trim(); // Query existing files const files = useQuery( @@ -378,6 +393,17 @@ export default function ProjectSettings() { !horizonAppSecretValid || savingHorizon; + const amazonSharedSecretNeeded = + !hasAmazonSharedSecretConfigured || isReplacingAmazonSharedSecret; + const amazonSharedSecretValid = + !amazonSharedSecretNeeded || + (trimmedAmazonSharedSecret.length > 0 && + trimmedAmazonSharedSecret.length <= 2_048); + const amazonHasChanges = + amazonSharedSecretNeeded && trimmedAmazonSharedSecret.length > 0; + const disableSaveAmazon = + !amazonHasChanges || !amazonSharedSecretValid || savingAmazon; + const handleMetadataSubmit = async (event: FormEvent) => { event.preventDefault(); if (!project || disableSaveMetadata) { @@ -476,6 +502,29 @@ export default function ProjectSettings() { } }; + const handleAmazonSubmit = async () => { + if (!project || disableSaveAmazon) { + return; + } + + setSavingAmazon(true); + try { + await updateProject({ + projectId: project._id, + amazonSharedSecret: trimmedAmazonSharedSecret, + }); + + setAmazonSharedSecret(""); + setIsReplacingAmazonSharedSecret(false); + toast.success("Amazon RVS configuration saved."); + } catch (error: any) { + console.error("Amazon RVS config update error:", error); + toast.error(error.message || "Failed to save Amazon RVS configuration."); + } finally { + setSavingAmazon(false); + } + }; + const handleReportingCurrencySubmit = async ( event: FormEvent, ) => { @@ -1789,6 +1838,88 @@ export default function ProjectSettings() { )} + + {/* Amazon Appstore subsection — Fire OS builds use + Amazon's Appstore SDK and IAPKit verifies receipts + server-side through RVS with a project-level shared + secret. */} +
    +
    + + {hasAmazonSharedSecretConfigured && + !isReplacingAmazonSharedSecret ? ( +
    +
    + + + {"Shared Secret configured"} + +
    + +
    + ) : ( + + setAmazonSharedSecret(e.target.value) + } + className="w-full px-3 py-2 rounded-lg border border-border bg-background text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary" + /> + )} +

    + { + "Required for production Amazon Appstore receipt verification. App Tester sandbox requests can run without exposing this secret to clients." + } +

    + {!amazonSharedSecretValid && + trimmedAmazonSharedSecret.length > 0 && ( +

    + { + "Shared Secret must be 1–2048 characters after trimming." + } +

    + )} +
    + +
    + + {"Amazon RVS docs"} + + + +
    +
    )} diff --git a/packages/kit/src/pages/auth/organization/project/subscriptions.tsx b/packages/kit/src/pages/auth/organization/project/subscriptions.tsx index a3ce095b..f7309793 100644 --- a/packages/kit/src/pages/auth/organization/project/subscriptions.tsx +++ b/packages/kit/src/pages/auth/organization/project/subscriptions.tsx @@ -21,7 +21,10 @@ import { normalizeCurrencyCode, } from "@/lib/utils"; -type DashboardProject = Omit, "apiKey" | "horizonAppSecret">; +type DashboardProject = Omit< + Doc<"projects">, + "apiKey" | "horizonAppSecret" | "amazonSharedSecret" +>; type ProjectContext = { project: DashboardProject }; const STATE_FILTERS = [ diff --git a/packages/kit/src/pages/auth/organization/project/webhooks.tsx b/packages/kit/src/pages/auth/organization/project/webhooks.tsx index ab097df4..6af95314 100644 --- a/packages/kit/src/pages/auth/organization/project/webhooks.tsx +++ b/packages/kit/src/pages/auth/organization/project/webhooks.tsx @@ -15,7 +15,10 @@ import { api } from "@/convex"; import { PageLoading } from "@/components/LoadingSpinner"; type ProjectContext = { - project: Omit, "apiKey" | "horizonAppSecret">; + project: Omit< + Doc<"projects">, + "apiKey" | "horizonAppSecret" | "amazonSharedSecret" + >; }; export default function ProjectWebhooks() { @@ -70,7 +73,7 @@ export default function ProjectWebhooks() { {setup ? ( -
    +
    +
    ) : null} diff --git a/packages/kit/src/pages/blog/iapkit-joins-openiap.tsx b/packages/kit/src/pages/blog/iapkit-joins-openiap.tsx index 5140abee..44f13553 100644 --- a/packages/kit/src/pages/blog/iapkit-joins-openiap.tsx +++ b/packages/kit/src/pages/blog/iapkit-joins-openiap.tsx @@ -28,7 +28,7 @@ const FAQ: Array<{ q: string; a: string }> = [ }, { q: "Which platforms does IAPKit support?", - a: "IAPKit validates receipts for Apple App Store, Google Play, and Meta Horizon. Amazon Vega OS and FireOS support is on the roadmap.", + a: "IAPKit validates receipts for Apple App Store, Google Play, and Meta Horizon. Fire OS and Vega OS receipt-validation support is on the roadmap.", }, ]; @@ -129,8 +129,8 @@ export default function IapkitJoinsOpenIap() { each platform makes in-app purchase validation harder than it needs to be, and each in its own way. Different APIs, different edge cases, different ways to fail. Every IAP developer reinventing the same - wheel. And with new platforms like Amazon Vega OS and FireOS joining - the ecosystem, the fragmentation is only getting worse. + wheel. And with new platforms like Fire OS and Vega OS + joining the ecosystem, the fragmentation is only getting worse.

    @@ -245,8 +245,8 @@ export default function IapkitJoinsOpenIap() { Billing v7+, Meta Horizon.

  • - New platform support coming — Amazon Vega OS, - FireOS. + New receipt-validation support coming — Fire OS, + Vega OS.
  • Server-side webhooks and real-time notifications — renewals, diff --git a/packages/kit/src/pages/docs/sections/api.tsx b/packages/kit/src/pages/docs/sections/api.tsx index da7e7e73..869f4f31 100644 --- a/packages/kit/src/pages/docs/sections/api.tsx +++ b/packages/kit/src/pages/docs/sections/api.tsx @@ -91,14 +91,24 @@ export default function ApiReferencePage() { }`} +

    Amazon Appstore variant

    + + {`{ + "store": "amazon", + "userId": "amzn1.account.ABC123", // Amazon user id (≤ 512 chars) + "receiptId": "amzn1.receipt.ABC123", // Amazon receipt id (≤ 4 KB) + "sandbox": true // App Tester / RVS sandbox +}`} + +

    The JSON body is capped at 32 KB before parsing. Every string field is then validated server-side for non-empty + per-field length bounds. Oversized fields return 400 INVALID_INPUT; oversized request bodies return 413 PAYLOAD_TOO_LARGE. Neither path - calls Apple / Google / Meta, so malformed clients don't burn your - upstream quota. + calls Apple / Google / Horizon / Amazon, so malformed clients don't + burn your upstream quota.

    @@ -112,11 +122,12 @@ export default function ApiReferencePage() {

    - Your app can unlock premium state when isValid === true.{" "} + Your app can unlock local premium state, or your backend can grant its + own entitlement, when isValid === true.{" "} state carries the harmonized lifecycle position across all - three stores, and productId is the product id verified by - the upstream store. For Meta Horizon, productId is the SKU - IAPKit checked. + supported stores, and productId is the product id verified + by the upstream store. For Meta Horizon, productId is the + SKU IAPKit checked.

    If your own backend keeps an entitlement ledger, do not trust a diff --git a/packages/kit/src/pages/docs/sections/quickstart.tsx b/packages/kit/src/pages/docs/sections/quickstart.tsx index 4bf996b2..9046ed2d 100644 --- a/packages/kit/src/pages/docs/sections/quickstart.tsx +++ b/packages/kit/src/pages/docs/sections/quickstart.tsx @@ -160,6 +160,18 @@ export default function QuickstartPage() { }'`} + + {`curl -X POST https://kit.openiap.dev/v1/purchase/verify \\ + -H "Authorization: Bearer openiap-kit_" \\ + -H "Content-Type: application/json" \\ + -d '{ + "store": "amazon", + "userId": "amzn1.account.ABC123", + "receiptId": "amzn1.receipt.ABC123456789", + "sandbox": true + }'`} + +

    Expected response:

    {`{ diff --git a/scripts/agent/compile-context.ts b/scripts/agent/compile-context.ts index 52666295..2f33ec0d 100644 --- a/scripts/agent/compile-context.ts +++ b/scripts/agent/compile-context.ts @@ -38,54 +38,75 @@ const CONFIG = { rootLlmsOutputDir: path.resolve(scriptDir, "../.."), }; -function readInstallationVersions(): { +type LlmsVersions = { apple: string; + flutter: string; google: string; godot: string; kmp: string; maui: string; mauiPackageId: string; -} { - const versionsPath = path.join(CONFIG.projectRoot, "openiap-versions.json"); - const versions = JSON.parse(fs.readFileSync(versionsPath, "utf-8")) as { - apple?: string; - google?: string; - }; - const kmpProperties = fs.readFileSync( - path.join(CONFIG.projectRoot, "libraries/kmp-iap/gradle.properties"), - "utf-8", - ); - const godotPlugin = fs.readFileSync( - path.join(CONFIG.projectRoot, "libraries/godot-iap/addons/godot-iap/plugin.cfg"), - "utf-8", - ); - const mauiProject = fs.readFileSync( - path.join(CONFIG.projectRoot, "libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj"), +}; + +function readJsonFile(relativePath: string): T { + return JSON.parse( + fs.readFileSync(path.join(CONFIG.projectRoot, relativePath), "utf-8"), + ) as T; +} + +function readRegexVersion( + relativePath: string, + pattern: RegExp, + label: string, +): string { + const content = fs.readFileSync( + path.join(CONFIG.projectRoot, relativePath), "utf-8", ); - const kmpVersion = kmpProperties.match(/^libraryVersion=(.+)$/m)?.[1]?.trim(); - const godotVersion = godotPlugin.match(/^version="([^"]+)"$/m)?.[1]?.trim(); - const mauiPackageId = mauiProject.match(/([^<]+)<\/PackageId>/)?.[1]?.trim(); - const mauiVersion = mauiProject.match(/([^<]+)<\/PackageVersion>/)?.[1]?.trim(); - - if (!versions.apple || !versions.google) { - throw new Error("openiap-versions.json must include apple and google versions"); - } - if (!godotVersion || !kmpVersion || !mauiPackageId || !mauiVersion) { - throw new Error("Framework package metadata is missing godot, kmp, or maui values"); + const version = content.match(pattern)?.[1]?.trim(); + if (!version) { + throw new Error(`Unable to resolve ${label} version from ${relativePath}`); } + return version; +} + +function readInstallationVersions(): LlmsVersions { + const openiapVersions = readJsonFile<{ apple: string; google: string }>( + "openiap-versions.json", + ); return { - apple: versions.apple, - godot: godotVersion, - google: versions.google, - kmp: kmpVersion, - maui: mauiVersion, - mauiPackageId, + apple: openiapVersions.apple, + google: openiapVersions.google, + flutter: readRegexVersion( + "libraries/flutter_inapp_purchase/pubspec.yaml", + /^version:\s*([^\s]+)/m, + "flutter_inapp_purchase", + ), + godot: readRegexVersion( + "libraries/godot-iap/addons/godot-iap/plugin.cfg", + /^version="([^"]+)"$/m, + "godot-iap", + ), + kmp: readRegexVersion( + "libraries/kmp-iap/gradle.properties", + /^libraryVersion=(.+)$/m, + "kmp-iap", + ), + maui: readRegexVersion( + "libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj", + /([^<]+)<\/PackageVersion>/, + "OpenIap.Maui", + ), + mauiPackageId: readRegexVersion( + "libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj", + /([^<]+)<\/PackageId>/, + "OpenIap.Maui package id", + ), }; } -function withSingleTrailingNewline(content: string): string { +function withFinalNewline(content: string): string { return `${content.trimEnd()}\n`; } @@ -96,6 +117,7 @@ function withSingleTrailingNewline(content: string): string { async function generateLlmsTxt(): Promise<{ quick: number; full: number }> { console.log(chalk.blue("\n🤖 Generating llms.txt files...\n")); const versions = readInstallationVersions(); + const generatedAt = new Date().toISOString(); // Read all external API docs const externalFiles = await glob( @@ -109,15 +131,16 @@ async function generateLlmsTxt(): Promise<{ quick: number; full: number }> { > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Quick Reference: https://openiap.dev/llms.txt -> Generated: ${new Date().toISOString()} +> Generated: ${generatedAt} ## Table of Contents 1. Installation 2. Core APIs (Connection, Products, Purchase, Subscription) 3. Platform-Specific APIs (iOS, Android) -4. Types Reference -5. Error Codes & Handling -6. Implementation Patterns +4. Store Targets (Play, Horizon, Fire OS, Vega OS) +5. Types Reference +6. Error Codes & Handling +7. Implementation Patterns --- @@ -149,6 +172,9 @@ implementation("io.github.hyochan.openiap:openiap-google:${versions.google}") // For Meta Horizon OS implementation("io.github.hyochan.openiap:openiap-google-horizon:${versions.google}") + +// For Fire OS (Amazon Appstore) +implementation("io.github.hyochan.openiap:openiap-google-amazon:${versions.google}") \`\`\` ### Flutter @@ -189,6 +215,9 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. \`packages/google\`. - Public surface: generated OpenIAP types plus \`useIAP\`, listener helpers, and platform-suffixed iOS/Android APIs. +- Android builds can select Play, Horizon, or Fire OS artifacts. + Vega OS resolves a \`kepler\` JavaScript adapter before creating the Nitro + HybridObject. - Example app: \`libraries/react-native-iap/example\`. ### expo-iap @@ -196,6 +225,9 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. - Implementation: Expo Modules wrapper over the same native OpenIAP packages. - Public surface: same hook, listener, query, mutation, and platform API shape as \`react-native-iap\`, adapted for Expo managed/bare workflows. +- Config plugins can select Horizon or Fire OS Android flavors; + Vega OS follows the Onside-style runtime selector pattern with a JavaScript + adapter. - Example app: \`libraries/expo-iap/example\`. ### flutter_inapp_purchase @@ -204,6 +236,7 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. iOS and Android method channels. - Public surface: singleton \`FlutterInappPurchase.instance\`, typed \`fetchProducts\`, purchase streams, and resolver-style methods. +- Android builds can select Play, Horizon, or Fire OS flavors. ### godot-iap - Package: \`godot-iap\` for Godot 4.x. @@ -242,6 +275,48 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. --- +## Store Targets + +- Google Play: default Android artifact, \`openiap-google\`. +- Meta Horizon: Android \`horizon\` flavor, \`openiap-google-horizon\`. +- Fire OS: Android \`amazon\` flavor, + \`openiap-google-amazon\`; set \`fireOsEnabled=true\` or + \`missingDimensionStrategy("platform", "amazon")\`. + Runtime adapters are wired for native Android, \`react-native-iap\`, + \`expo-iap\`, and \`flutter_inapp_purchase\`; Godot, KMP, and MAUI have schema + type parity but still need Android wrapper flavor switches. +- Vega OS: not an Android flavor. Target React Native for Vega / Expo only, + using Amazon's JavaScript IAP API through the runtime-selected \`kepler\` + adapter at the same runtime integration layer as Onside. In Expo or React + Native config plugin options, \`modules.vega=true\` is only a + runtime-support guard; it does not select an Android flavor and cannot be + combined with \`modules.fireOS\` or \`modules.horizon\`. + +### Fire OS + +Fire OS is an Android target for Amazon Appstore distribution. It uses the +\`amazon\` Gradle flavor and Amazon Appstore SDK. + +Fire OS maps OpenIAP calls to the Amazon Appstore SDK: + +| OpenIAP API | Amazon Appstore SDK mapping | +|-------------|--------------------------| +| \`initConnection()\` | Register \`PurchasingListener\`, request user data | +| \`fetchProducts()\` | \`PurchasingService.getProductData\` | +| \`requestPurchase()\` | \`PurchasingService.purchase\` | +| \`getAvailablePurchases()\` | \`PurchasingService.getPurchaseUpdates(reset=true)\` | +| \`finishTransaction()\` | \`PurchasingService.notifyFulfillment(..., FULFILLED)\` | + +### Vega OS Runtime + +Vega OS is not Fire OS and must not set \`fireOsEnabled=true\`; that flag is +only for Android Fire OS builds. Install +\`@amazon-devices/keplerscript-appstore-iap-lib\` and let +\`react-native-iap\` / \`expo-iap\` select the \`kepler\` adapter at runtime, +similar to how Onside is selected at the runtime integration layer. + +--- + ## Minimal Usage by Framework ### React Native / Expo @@ -352,7 +427,7 @@ await ((QueryResolver)iap).FetchProductsAsync(new ProductRequest > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Full Reference: https://openiap.dev/llms-full.txt -> Generated: ${new Date().toISOString()} +> Generated: ${generatedAt} ## Installation @@ -374,6 +449,8 @@ npm install react-native-iap \`\`\`kotlin // Gradle implementation("io.github.hyochan.openiap:openiap-google:${versions.google}") +implementation("io.github.hyochan.openiap:openiap-google-horizon:${versions.google}") +implementation("io.github.hyochan.openiap:openiap-google-amazon:${versions.google}") \`\`\` \`\`\`bash @@ -391,9 +468,9 @@ flutter pub add flutter_inapp_purchase implementation("io.github.hyochan:kmp-iap:${versions.kmp}") \`\`\` -\`\`\`bash -# .NET MAUI -dotnet add package ${versions.mauiPackageId} +\`\`\`xml + + \`\`\` Current NuGet package version: ${versions.maui} @@ -429,7 +506,7 @@ await endConnection(); \`\`\`typescript const products = await fetchProducts({ products: [ - { id: 'com.app.premium', type: 'inapp' }, + { id: 'com.app.premium', type: 'in-app' }, { id: 'com.app.monthly', type: 'subs' }, ], }); @@ -444,7 +521,7 @@ await requestPurchase({ apple: { sku: 'com.app.premium' }, google: { skus: ['com.app.premium'] }, }, - type: 'inapp', // 'inapp' | 'subs' + type: 'in-app', // 'in-app' | 'subs' }); \`\`\` @@ -452,7 +529,7 @@ await requestPurchase({ \`\`\`typescript // CRITICAL: Must call after verification // Android: purchases auto-refund after 3 days if not acknowledged -await finishTransaction(purchase, isConsumable); +await finishTransaction({ purchase, isConsumable }); \`\`\` ### Get Available Purchases @@ -477,7 +554,7 @@ const purchaseUpdateSubscription = purchaseUpdatedListener(async (purchase) => { // 1. Verify purchase on server // 2. Grant entitlement // 3. Finish transaction - await finishTransaction(purchase); + await finishTransaction({ purchase, isConsumable: false }); }); const purchaseErrorSubscription = purchaseErrorListener((error) => { @@ -501,7 +578,7 @@ interface Product { price: string; // Formatted price string priceAmount: number; // Price as number currency: string; // ISO 4217 currency code - type: 'inapp' | 'subs'; + type: 'in-app' | 'subs'; } \`\`\` @@ -586,11 +663,11 @@ interface PurchaseError { fs.mkdirSync(outputDir, { recursive: true }); fs.writeFileSync( path.join(outputDir, "llms.txt"), - withSingleTrailingNewline(quickContent), + withFinalNewline(quickContent), ); fs.writeFileSync( path.join(outputDir, "llms-full.txt"), - withSingleTrailingNewline(fullContent), + withFinalNewline(fullContent), ); } @@ -741,7 +818,7 @@ openiap/ // ========================================================================= const outputPath = path.join(CONFIG.outputDir, CONFIG.outputFile); - fs.writeFileSync(outputPath, withSingleTrailingNewline(output)); + fs.writeFileSync(outputPath, withFinalNewline(output)); // ========================================================================= // Generate LLMs.txt Files @@ -777,7 +854,9 @@ openiap/ ), ); console.log( - chalk.green(` ✓ Output: ${path.join(CONFIG.rootLlmsOutputDir, "llms.txt")}`), + chalk.green( + ` ✓ Output: ${path.join(CONFIG.rootLlmsOutputDir, "llms.txt")}`, + ), ); console.log( chalk.green( From 5ebe5f26a187441b1b718ace537cd11c25c01670 Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 25 May 2026 02:53:48 +0900 Subject: [PATCH 02/51] fix(runtime): address vega review feedback Use stable fallback timestamps, avoid replaceAll in Vega adapters, and replace Amazon SDK reflection with public accessors. Validation: - libraries/expo-iap: bun run lint:tsc - libraries/expo-iap: bun test src/__tests__/vega-adapter.test.ts - libraries/react-native-iap: tsc -p tsconfig.json --noEmit --skipLibCheck - packages/google: ./gradlew :openiap:compileAmazonDebugKotlin --- libraries/expo-iap/src/vega-adapter.ts | 6 +++--- libraries/react-native-iap/src/vega-adapter.ts | 6 +++--- .../amazon/java/dev/hyo/openiap/OpenIapModule.kt | 16 +++------------- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/libraries/expo-iap/src/vega-adapter.ts b/libraries/expo-iap/src/vega-adapter.ts index 7c8e4382..0793d835 100644 --- a/libraries/expo-iap/src/vega-adapter.ts +++ b/libraries/expo-iap/src/vega-adapter.ts @@ -248,9 +248,9 @@ function toTimestamp(value: unknown): number { if (typeof value === 'number' && Number.isFinite(value)) return value; if (typeof value === 'string') { const timestamp = Date.parse(value); - return Number.isFinite(timestamp) ? timestamp : Date.now(); + return Number.isFinite(timestamp) ? timestamp : 0; } - return Date.now(); + return 0; } function toPriceAmountMicros(value: unknown): string { @@ -531,7 +531,7 @@ export function createExpoIapVegaModule( function normalizeIapkitState(state: unknown): IapkitPurchaseState { const normalized = typeof state === 'string' - ? state.toLowerCase().replaceAll('_', '-') + ? state.toLowerCase().replace(/_/g, '-') : 'unknown'; const states = new Set([ 'entitled', diff --git a/libraries/react-native-iap/src/vega-adapter.ts b/libraries/react-native-iap/src/vega-adapter.ts index 7f556d08..11d7b0a3 100644 --- a/libraries/react-native-iap/src/vega-adapter.ts +++ b/libraries/react-native-iap/src/vega-adapter.ts @@ -229,9 +229,9 @@ function toTimestamp(value: unknown): number { if (typeof value === 'number' && Number.isFinite(value)) return value; if (typeof value === 'string') { const timestamp = Date.parse(value); - return Number.isFinite(timestamp) ? timestamp : Date.now(); + return Number.isFinite(timestamp) ? timestamp : 0; } - return Date.now(); + return 0; } function toPriceAmountMicros(value: unknown): string { @@ -553,7 +553,7 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { function normalizeIapkitState(state: unknown): IapkitPurchaseState { const normalized = typeof state === 'string' - ? state.toLowerCase().replaceAll('_', '-') + ? state.toLowerCase().replace(/_/g, '-') : 'unknown'; const states = new Set([ 'entitled', diff --git a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt index 49b7a21f..a2612a1c 100644 --- a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt @@ -660,12 +660,7 @@ class OpenIapModule( } private fun updateStorefront(userData: UserData?) { - val countryCode = userData?.let { - runCatching { - val method = it.javaClass.getMethod("getCountryCode") - method.invoke(it) as? String - }.getOrNull() - } + val countryCode = userData?.countryCode storefrontCode = countryCode ?: userData?.marketplace ?: storefrontCode @@ -688,9 +683,10 @@ class OpenIapModule( } private fun AmazonProduct.toSubscriptionProduct(): ProductSubscriptionAndroid { + val subscriptionPeriod = this.subscriptionPeriod val phase = PricingPhaseAndroid( billingCycleCount = 0, - billingPeriod = reflectedString("getSubscriptionPeriod").orEmpty(), + billingPeriod = subscriptionPeriod.orEmpty(), formattedPrice = price.orEmpty(), priceAmountMicros = "0", priceCurrencyCode = "", @@ -759,10 +755,4 @@ class OpenIapModule( ) } - private fun AmazonProduct.reflectedString(methodName: String): String? { - return runCatching { - val method = javaClass.getMethod(methodName) - method.invoke(this) as? String - }.getOrNull() - } } From f85e0b51df2f514c856ee1f0d64b6128ab14afbd Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 25 May 2026 03:14:59 +0900 Subject: [PATCH 03/51] fix(runtime): address amazon review feedback Persist Amazon RVS parse failures, add request timeouts, filter canceled receipts, and align Kit verify responses with the documented store field. --- .github/workflows/release-google.yml | 2 +- .../src/__tests__/vega-adapter.test.ts | 243 ++++++++++++++++++ libraries/expo-iap/src/vega-adapter.ts | 139 +++++++--- .../addons/godot-iap/android/GodotIap.gdap | 2 +- .../java/dev/hyo/openiap/OpenIapModule.kt | 64 ++++- packages/kit/convex/purchases/amazon.test.ts | 21 +- packages/kit/convex/purchases/amazon.ts | 137 +++++++--- packages/kit/convex/purchases/errors.ts | 4 +- packages/kit/server/api/v1/replay-guard.ts | 4 +- .../kit/server/api/v1/request-logger.test.ts | 9 +- .../kit/server/api/v1/route-input-schemas.ts | 2 +- .../api/v1/route-response-schemas.test.ts | 39 +++ .../server/api/v1/route-response-schemas.ts | 10 +- packages/kit/server/api/v1/routes.test.ts | 1 + packages/kit/server/api/v1/routes.ts | 20 +- .../auth/organization/project/settings.tsx | 6 + .../src/pages/blog/iapkit-joins-openiap.tsx | 14 +- packages/kit/src/pages/docs/sections/api.tsx | 3 +- .../src/pages/docs/sections/quickstart.tsx | 3 +- 19 files changed, 607 insertions(+), 116 deletions(-) create mode 100644 packages/kit/server/api/v1/route-response-schemas.test.ts diff --git a/.github/workflows/release-google.yml b/.github/workflows/release-google.yml index 0b900c50..1a9b24fe 100644 --- a/.github/workflows/release-google.yml +++ b/.github/workflows/release-google.yml @@ -295,7 +295,7 @@ jobs: env: VERSION: ${{ steps.version.outputs.version }} run: | - if curl -s "https://repo1.maven.org/maven2/io/github/hyochan/openiap/openiap-google-amazon/$VERSION/" | grep -q "$VERSION"; then + if curl -fsI "https://repo1.maven.org/maven2/io/github/hyochan/openiap/openiap-google-amazon/$VERSION/" >/dev/null; then echo "exists=true" >> $GITHUB_OUTPUT echo "⚠️ openiap-google-amazon $VERSION already exists on Maven Central" else diff --git a/libraries/expo-iap/src/__tests__/vega-adapter.test.ts b/libraries/expo-iap/src/__tests__/vega-adapter.test.ts index 5858fb89..7c2d8cb0 100644 --- a/libraries/expo-iap/src/__tests__/vega-adapter.test.ts +++ b/libraries/expo-iap/src/__tests__/vega-adapter.test.ts @@ -415,6 +415,30 @@ describe('Amazon Vega Expo adapter', () => { } }); + it('rejects mixed IAPKit payloads on the Amazon Vega adapter', async () => { + const service = createService(); + const module = createExpoIapVegaModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + google: { + purchaseToken: 'google-token', + }, + }, + }), + ).rejects.toMatchObject({ + code: ErrorCode.DeveloperError, + message: + 'Amazon Vega IAPKit verification requires exactly one amazon payload.', + }); + }); + it('wraps non-JSON IAPKit failures as receipt errors', async () => { const service = createService(); const originalFetch = globalThis.fetch; @@ -446,6 +470,44 @@ describe('Amazon Vega Expo adapter', () => { } }); + it('extracts nested JSON IAPKit failure messages', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + Response.json( + { + message: JSON.stringify({ + error: 'receipt no longer valid', + }), + }, + {status: 400}, + ), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createExpoIapVegaModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toMatchObject({ + code: ErrorCode.ReceiptFailed, + message: 'receipt no longer valid', + }); + } finally { + globalThis.fetch = originalFetch; + } + }); + it('rejects empty successful IAPKit responses as receipt errors', async () => { const service = createService(); const originalFetch = globalThis.fetch; @@ -477,6 +539,153 @@ describe('Amazon Vega Expo adapter', () => { } }); + it('treats successful IAPKit error payloads as receipt errors', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + Response.json( + { + errors: [ + { + code: 'BAD_RECEIPT', + message: 'bad receipt', + }, + ], + }, + {status: 200}, + ), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createExpoIapVegaModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toMatchObject({ + code: ErrorCode.ReceiptFailed, + message: 'bad receipt', + }); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('rejects malformed successful IAPKit payloads', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + Response.json(['not', 'an', 'object'], {status: 200}), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createExpoIapVegaModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toMatchObject({ + code: ErrorCode.ReceiptFailed, + message: 'IAPKit returned malformed response (HTTP 200).', + }); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('rejects successful IAPKit payloads missing required fields', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + Response.json( + { + state: 'ENTITLED', + store: 'amazon', + }, + {status: 200}, + ), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createExpoIapVegaModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toMatchObject({ + code: ErrorCode.ReceiptFailed, + message: 'IAPKit returned malformed response (HTTP 200).', + }); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('rejects successful IAPKit payloads for another store', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + Response.json( + { + isValid: true, + state: 'ENTITLED', + store: 'apple', + }, + {status: 200}, + ), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createExpoIapVegaModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toMatchObject({ + code: ErrorCode.ReceiptFailed, + message: 'IAPKit returned malformed response (HTTP 200).', + }); + } finally { + globalThis.fetch = originalFetch; + } + }); + it('wraps IAPKit network failures as network errors', async () => { const service = createService(); const originalFetch = globalThis.fetch; @@ -506,4 +715,38 @@ describe('Amazon Vega Expo adapter', () => { globalThis.fetch = originalFetch; } }); + + it('wraps IAPKit response body read failures as network errors', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn(async () => ({ + ok: true, + status: 200, + text: async () => { + throw new TypeError('body stream failed'); + }, + })) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createExpoIapVegaModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toMatchObject({ + code: ErrorCode.NetworkError, + message: 'body stream failed', + }); + } finally { + globalThis.fetch = originalFetch; + } + }); }); diff --git a/libraries/expo-iap/src/vega-adapter.ts b/libraries/expo-iap/src/vega-adapter.ts index 0793d835..1bf63de7 100644 --- a/libraries/expo-iap/src/vega-adapter.ts +++ b/libraries/expo-iap/src/vega-adapter.ts @@ -17,6 +17,8 @@ type ResponseOperation = | 'user-data' | 'notify-fulfillment'; +const IAPKIT_VERIFY_TIMEOUT_MS = 10_000; + type VegaListener = (payload: any) => void; interface VegaPurchaseErrorPayload { @@ -500,7 +502,11 @@ export function createExpoIapVegaModule( const receipts = await getPurchaseUpdateReceipts(); await hydrateProductTypesForReceipts(receipts); return receipts - .filter((receipt) => includeSuspended || !receipt.isDeferred) + .filter((receipt) => { + const isCanceled = Boolean(receipt.isCancelled || receipt.cancelDate); + if (isCanceled) return false; + return includeSuspended || !receipt.isDeferred; + }) .map((receipt) => mapReceipt(receipt, getCachedProductType(receipt, productTypesBySku)), ); @@ -552,19 +558,23 @@ export function createExpoIapVegaModule( function extractIapkitErrorMessage(json: unknown): string | null { if (!json || typeof json !== 'object') return null; const record = json as Record; + function extractStringMessage(value: string): string { + try { + const parsed = JSON.parse(value); + return parsed && typeof parsed === 'object' + ? (extractIapkitErrorMessage(parsed) ?? value) + : value; + } catch { + return value; + } + } + const details = record.details; if (details && typeof details === 'object') { const originalError = (details as Record) .originalError; if (typeof originalError === 'string') { - try { - return ( - extractIapkitErrorMessage(JSON.parse(originalError)) ?? - originalError - ); - } catch { - return originalError; - } + return extractStringMessage(originalError); } } @@ -573,11 +583,13 @@ export function createExpoIapVegaModule( return extractIapkitErrorMessage(errors[0]); } - return typeof record.message === 'string' - ? record.message - : typeof record.error === 'string' - ? record.error - : null; + if (typeof record.message === 'string') { + return extractStringMessage(record.message); + } + if (typeof record.error === 'string') { + return extractStringMessage(record.error); + } + return null; } function parseIapkitJsonResponse(text: string): unknown | null { @@ -589,6 +601,42 @@ export function createExpoIapVegaModule( } } + function isIapkitResultObject( + json: unknown, + ): json is Record { + return Boolean(json) && typeof json === 'object' && !Array.isArray(json); + } + + function hasIapkitErrors(json: unknown): boolean { + if (!isIapkitResultObject(json)) return false; + const errors = json.errors; + return Array.isArray(errors) && errors.length > 0; + } + + function readIapkitResult( + json: Record, + status: number, + ): { + isValid: boolean; + state: IapkitPurchaseState; + } { + if ( + typeof json.isValid !== 'boolean' || + typeof json.state !== 'string' || + json.store !== 'amazon' + ) { + throw createVegaError( + ErrorCode.ReceiptFailed, + `IAPKit returned malformed response (HTTP ${status}).`, + ); + } + + return { + isValid: json.isValid, + state: normalizeIapkitState(json.state), + }; + } + if (options.provider !== 'iapkit') { throw createVegaError( ErrorCode.FeatureNotSupported, @@ -597,15 +645,20 @@ export function createExpoIapVegaModule( } const iapkit = options.iapkit; + const payloadCount = + Number(Boolean(iapkit?.amazon)) + + Number(Boolean(iapkit?.apple)) + + Number(Boolean(iapkit?.google)); const amazon = iapkit?.amazon; - if (!amazon) { + if (payloadCount !== 1 || !amazon) { throw createVegaError( ErrorCode.DeveloperError, - 'Amazon Vega IAPKit verification requires amazon parameters.', + 'Amazon Vega IAPKit verification requires exactly one amazon payload.', ); } - const receiptId = amazon.receiptId.trim(); + const receiptId = + typeof amazon.receiptId === 'string' ? amazon.receiptId.trim() : ''; if (!receiptId) { throw createVegaError( ErrorCode.DeveloperError, @@ -613,7 +666,7 @@ export function createExpoIapVegaModule( ); } - let userId = amazon.userId?.trim() ?? ''; + let userId = typeof amazon.userId === 'string' ? amazon.userId.trim() : ''; if (!userId) { const response = await service.getUserData({}); ensureSuccessful( @@ -631,15 +684,20 @@ export function createExpoIapVegaModule( ); } + const apiKey = + typeof iapkit?.apiKey === 'string' ? iapkit.apiKey.trim() : ''; let response: Response; try { + const controller = new AbortController(); + const timeout = setTimeout( + () => controller.abort(), + IAPKIT_VERIFY_TIMEOUT_MS, + ); response = await fetch(IAPKIT_VERIFY_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', - ...(iapkit?.apiKey - ? {Authorization: `Bearer ${iapkit.apiKey}`} - : {}), + ...(apiKey ? {Authorization: `Bearer ${apiKey}`} : {}), }, body: JSON.stringify({ store: 'amazon', @@ -647,7 +705,8 @@ export function createExpoIapVegaModule( receiptId, ...(amazon.sandbox == null ? {} : {sandbox: amazon.sandbox}), }), - }); + signal: controller.signal, + }).finally(() => clearTimeout(timeout)); } catch (error) { throw createVegaError( ErrorCode.NetworkError, @@ -656,7 +715,17 @@ export function createExpoIapVegaModule( : 'Failed to reach IAPKit verification endpoint.', ); } - const text = await response.text(); + let text: string; + try { + text = await response.text(); + } catch (error) { + throw createVegaError( + ErrorCode.NetworkError, + error instanceof Error + ? error.message + : 'Failed to read IAPKit verification response.', + ); + } const json = parseIapkitJsonResponse(text); if (!response.ok) { @@ -673,16 +742,26 @@ export function createExpoIapVegaModule( ); } - const result = json as { - isValid?: unknown; - state?: unknown; - store?: unknown; - }; + if (!isIapkitResultObject(json)) { + throw createVegaError( + ErrorCode.ReceiptFailed, + `IAPKit returned malformed response (HTTP ${response.status}).`, + ); + } + + if (hasIapkitErrors(json)) { + throw createVegaError( + ErrorCode.ReceiptFailed, + extractIapkitErrorMessage(json) ?? 'IAPKit verification failed.', + ); + } + + const result = readIapkitResult(json, response.status); return { provider: 'iapkit', iapkit: { - isValid: result.isValid === true, - state: normalizeIapkitState(result.state), + isValid: result.isValid, + state: result.state, store: 'amazon', }, }; diff --git a/libraries/godot-iap/addons/godot-iap/android/GodotIap.gdap b/libraries/godot-iap/addons/godot-iap/android/GodotIap.gdap index c25cd3ad..fa6b1b0c 100644 --- a/libraries/godot-iap/addons/godot-iap/android/GodotIap.gdap +++ b/libraries/godot-iap/addons/godot-iap/android/GodotIap.gdap @@ -5,4 +5,4 @@ binary="GodotIap.release.aar" [dependencies] local=[] -remote=["io.github.hyochan.openiap:openiap-google:2.2.1", "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"] +remote=["io.github.hyochan.openiap:openiap-google:2.3.0-rc.1", "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"] diff --git a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt index a2612a1c..09369475 100644 --- a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import java.lang.ref.WeakReference +import java.util.Locale import java.util.concurrent.ConcurrentHashMap import com.amazon.device.iap.model.Product as AmazonProduct import com.amazon.device.iap.model.ProductType as AmazonProductType @@ -370,9 +371,13 @@ class OpenIapModule( val options = it.iapkit ?: throw OpenIapError.DeveloperError( "Missing IAPKit verification parameters" ) - val amazon = options.amazon ?: throw OpenIapError.DeveloperError( - "Amazon IAPKit verification requires amazon parameters" - ) + val payloadCount = listOfNotNull(options.apple, options.google, options.amazon).size + val amazon = options.amazon + if (payloadCount != 1 || amazon == null) { + throw OpenIapError.DeveloperError( + "Amazon IAPKit verification requires exactly one amazon payload" + ) + } val resolvedOptions = if (amazon.userId.isNullOrBlank()) { val userDataResponse = requestUserData() val userId = userDataResponse.userData?.userId @@ -596,11 +601,13 @@ class OpenIapModule( shouldReset = false when (response.requestStatus) { PurchaseUpdatesResponse.RequestStatus.SUCCESSFUL -> { - purchases += response.receipts.orEmpty().map { receipt -> - purchaseTypeByReceiptId[receipt.receiptId] = receipt.productType - productTypeBySku[receipt.sku] = receipt.productType - receipt.toPurchase() - } + purchases += response.receipts.orEmpty() + .filter { !it.isCanceled } + .map { receipt -> + purchaseTypeByReceiptId[receipt.receiptId] = receipt.productType + productTypeBySku[receipt.sku] = receipt.productType + receipt.toPurchase() + } } PurchaseUpdatesResponse.RequestStatus.NOT_SUPPORTED -> { throw OpenIapError.FeatureNotSupported("Amazon Appstore IAP is not supported on this device") @@ -626,13 +633,14 @@ class OpenIapModule( pending: ConcurrentHashMap>, earlyResponses: ConcurrentHashMap ): T { - earlyResponses.remove(requestId)?.let { return it } + val earlyResponse = earlyResponses.remove(requestId) + if (earlyResponse != null) return earlyResponse val deferred = CompletableDeferred() pending[requestId] = deferred earlyResponses.remove(requestId)?.let { response -> pending.remove(requestId) - if (!deferred.isCompleted) deferred.complete(response) + return response } return try { @@ -641,7 +649,6 @@ class OpenIapModule( throw OpenIapError.ServiceTimeout("Amazon Appstore request timed out") } finally { pending.remove(requestId) - earlyResponses.remove(requestId) } } @@ -683,10 +690,10 @@ class OpenIapModule( } private fun AmazonProduct.toSubscriptionProduct(): ProductSubscriptionAndroid { - val subscriptionPeriod = this.subscriptionPeriod + val subscriptionPeriod = this.subscriptionPeriod.toIsoBillingPeriod() val phase = PricingPhaseAndroid( billingCycleCount = 0, - billingPeriod = subscriptionPeriod.orEmpty(), + billingPeriod = subscriptionPeriod, formattedPrice = price.orEmpty(), priceAmountMicros = "0", priceCurrencyCode = "", @@ -709,7 +716,7 @@ class OpenIapModule( offerTokenAndroid = "", paymentMode = PaymentMode.PayAsYouGo, period = null, - price = 0.0, + price = price.toPriceAmount(), pricingPhasesAndroid = phases, type = DiscountOfferType.Introductory ) @@ -730,6 +737,35 @@ class OpenIapModule( ) } + private fun String?.toIsoBillingPeriod(): String { + val value = this?.trim().orEmpty() + if (value.isEmpty() || value.startsWith("P")) return value + + return when (value.lowercase(Locale.ROOT)) { + "weekly", "week", "1 week" -> "P1W" + "monthly", "month", "1 month" -> "P1M" + "quarterly", "quarter", "3 months" -> "P3M" + "semiannual", "semiannually", "semi-annual", "semi-annually", "6 months" -> "P6M" + "annual", "annually", "yearly", "year", "1 year" -> "P1Y" + else -> value + } + } + + private fun String?.toPriceAmount(): Double { + val value = this?.trim().orEmpty() + if (value.isEmpty()) return 0.0 + + val numeric = value.replace(Regex("[^0-9,.-]"), "") + if (numeric.isBlank()) return 0.0 + + val normalized = if (numeric.contains(',') && !numeric.contains('.')) { + numeric.replace(',', '.') + } else { + numeric.replace(",", "") + } + return normalized.toDoubleOrNull() ?: 0.0 + } + private fun AmazonReceipt.toPurchase(): PurchaseAndroid { val isSubscription = productType == AmazonProductType.SUBSCRIPTION val dateMillis = purchaseDate?.time?.toDouble() ?: 0.0 diff --git a/packages/kit/convex/purchases/amazon.test.ts b/packages/kit/convex/purchases/amazon.test.ts index 0d15d305..4f1bc2ca 100644 --- a/packages/kit/convex/purchases/amazon.test.ts +++ b/packages/kit/convex/purchases/amazon.test.ts @@ -51,16 +51,17 @@ describe("mapAmazonReceiptState", () => { }); test("maps expired subscription renewalDate to expired", () => { - vi.spyOn(Date, "now").mockReturnValue(2_000); - - expect( - mapAmazonReceiptState({ - productType: "SUBSCRIPTION", - renewalDate: 1_000, - }), - ).toBe(HarmonizedPurchaseState.EXPIRED); - - vi.restoreAllMocks(); + const nowSpy = vi.spyOn(Date, "now").mockReturnValue(2_000); + try { + expect( + mapAmazonReceiptState({ + productType: "SUBSCRIPTION", + renewalDate: 1_000, + }), + ).toBe(HarmonizedPurchaseState.EXPIRED); + } finally { + nowSpy.mockRestore(); + } }); test("falls back to unknown for unrecognized product types", () => { diff --git a/packages/kit/convex/purchases/amazon.ts b/packages/kit/convex/purchases/amazon.ts index 7bca4afa..f159a2aa 100644 --- a/packages/kit/convex/purchases/amazon.ts +++ b/packages/kit/convex/purchases/amazon.ts @@ -25,6 +25,7 @@ import { const AMAZON_RVS_BASE_URL = "https://appstore-sdk.amazon.com"; const AMAZON_RVS_VERSION = "1.0"; const AMAZON_SANDBOX_SHARED_SECRET = "iapkit-sandbox"; +const AMAZON_RVS_FETCH_TIMEOUT_MS = 10_000; export interface AmazonReceiptData { autoRenewing?: boolean; @@ -43,11 +44,22 @@ export interface AmazonReceiptData { } function describeError(error: unknown): string { + if (error instanceof ReceiptVerificationError) { + return error.errorMessage; + } const status = (error as { code?: unknown })?.code; const type = error instanceof Error ? error.name : typeof error; return typeof status === "number" ? `${type} ${status}` : type; } +function isAbortError(error: unknown): boolean { + return ( + error !== null && + typeof error === "object" && + (error as { name?: unknown }).name === "AbortError" + ); +} + function encodePathSegment(value: string): string { return encodeURIComponent(value); } @@ -116,6 +128,29 @@ export function parseAmazonReceiptResponse(raw: unknown): AmazonReceiptData { return raw; } +function parseAmazonJsonBody(bodyText: string): unknown { + const trimmed = bodyText.trim(); + if (!trimmed) { + throw new AmazonReceiptVerificationError( + "Amazon RVS returned an empty body.", + { responseBody: "" }, + ); + } + + try { + return JSON.parse(trimmed) as unknown; + } catch (error) { + throw new AmazonReceiptVerificationError( + "Amazon RVS returned invalid JSON.", + { + parseError: + error instanceof Error ? error.message : describeError(error), + responseBody: bodyText.slice(0, 2_048), + }, + ); + } +} + export const verifyAmazonReceiptInternalV1 = action({ args: { apiKey: v.string(), @@ -154,14 +189,44 @@ export const verifyAmazonReceiptInternalV1 = action({ sandbox, }); + const saveFailedReceipt = async (failure: { + error: string; + message: string; + details?: unknown; + state?: HarmonizedPurchaseState; + }) => { + await ctx.runMutation(internal.purchases.internal.saveReceiptInternal, { + projectId: project._id, + store: "amazon", + applicationId, + remoteId, + requestData, + remoteResponse: JSON.stringify({ + error: failure.error, + message: failure.message, + details: failure.details ?? null, + }), + state: failure.state ?? HarmonizedPurchaseState.UNKNOWN, + isValid: false, + requestIp: args.requestIp, + verificationDurationMs: Date.now() - verificationStart, + }); + }; + let parsedBody: unknown; try { parsedBody = await retryOnTransient( async () => { + const controller = new AbortController(); + const timeout = setTimeout( + () => controller.abort(), + AMAZON_RVS_FETCH_TIMEOUT_MS, + ); const res = await fetch(url, { method: "GET", headers: { Accept: "application/json" }, - }); + signal: controller.signal, + }).finally(() => clearTimeout(timeout)); const bodyText = await res.text().catch(() => ""); if (res.status === 400 || res.status === 497) { @@ -188,11 +253,13 @@ export const verifyAmazonReceiptInternalV1 = action({ throw err; } - return bodyText ? (JSON.parse(bodyText) as unknown) : {}; + return parseAmazonJsonBody(bodyText); }, { shouldRetry: (error) => - extractHttpStatus(error) === 429 || isTransientHttpError(error), + isAbortError(error) || + extractHttpStatus(error) === 429 || + isTransientHttpError(error), }, ); } catch (error) { @@ -201,47 +268,49 @@ export const verifyAmazonReceiptInternalV1 = action({ error.errorDetails?.status === 410 ? HarmonizedPurchaseState.CANCELED : HarmonizedPurchaseState.INAUTHENTIC; - await ctx.runMutation(internal.purchases.internal.saveReceiptInternal, { - projectId: project._id, - store: "amazon", - applicationId, - remoteId, - requestData, - remoteResponse: JSON.stringify({ - error: error.errorCode, - message: error.errorMessage, - details: error.errorDetails ?? null, - }), + await saveFailedReceipt({ + error: error.errorCode, + message: error.errorMessage, + details: error.errorDetails ?? null, state, - isValid: false, - requestIp: args.requestIp, - verificationDurationMs: Date.now() - verificationStart, }); return { isValid: false, state }; } const message = describeError(error); - await ctx.runMutation(internal.purchases.internal.saveReceiptInternal, { - projectId: project._id, - store: "amazon", - applicationId, - remoteId, - requestData, - remoteResponse: JSON.stringify({ - error: "AMAZON_RECEIPT_VERIFICATION_ERROR", - message, - }), - state: HarmonizedPurchaseState.UNKNOWN, - isValid: false, - requestIp: args.requestIp, - verificationDurationMs: Date.now() - verificationStart, + await saveFailedReceipt({ + error: + error instanceof ReceiptVerificationError + ? error.errorCode + : "AMAZON_RECEIPT_VERIFICATION_ERROR", + message, + details: + error instanceof ReceiptVerificationError + ? error.errorDetails + : undefined, }); throw new AmazonReceiptVerificationError(message); } - const receiptData = parseAmazonReceiptResponse(parsedBody); - const state = mapAmazonReceiptState(receiptData); - const remoteResponse = JSON.stringify(receiptData); + let receiptData: AmazonReceiptData; + let state: HarmonizedPurchaseState; + let remoteResponse: string; + try { + receiptData = parseAmazonReceiptResponse(parsedBody); + state = mapAmazonReceiptState(receiptData); + remoteResponse = JSON.stringify(receiptData); + } catch (error) { + const message = describeError(error); + await saveFailedReceipt({ + error: "AMAZON_RECEIPT_PARSE_ERROR", + message, + details: { + rawResponse: parsedBody, + stack: error instanceof Error ? error.stack : undefined, + }, + }); + throw new AmazonReceiptVerificationError(message); + } await ctx.runMutation(internal.purchases.internal.saveReceiptInternal, { projectId: project._id, diff --git a/packages/kit/convex/purchases/errors.ts b/packages/kit/convex/purchases/errors.ts index 4fdcb86d..11b657dc 100644 --- a/packages/kit/convex/purchases/errors.ts +++ b/packages/kit/convex/purchases/errors.ts @@ -86,11 +86,11 @@ export class AmazonReceiptInvalidError extends ReceiptVerificationError { } export class AmazonReceiptVerificationError extends ReceiptVerificationError { - constructor(detail: string) { + constructor(detail: string, details?: Record) { super( "AMAZON_RECEIPT_VERIFICATION_ERROR", `Amazon RVS verification failed: ${detail}`, - { originalError: detail }, + { originalError: detail, ...details }, ); } } diff --git a/packages/kit/server/api/v1/replay-guard.ts b/packages/kit/server/api/v1/replay-guard.ts index d57e2ebc..b8353dbe 100644 --- a/packages/kit/server/api/v1/replay-guard.ts +++ b/packages/kit/server/api/v1/replay-guard.ts @@ -35,7 +35,7 @@ export interface ReplayBucket { // returned `isValid: false`. Subsequent // requests for the exact same payload are short-circuited with // `REPEATED_FAILURE` until the cooldown expires — re-asking - // Apple / Google / Meta / Amazon about a receipt they already + // Apple / Google / Horizon / Amazon about a receipt they already // rejected, or retrying the same failed product-match guard, has // no chance of changing the answer in seconds. An attacker // replaying a captured-then-revoked receipt should hit a hard wall @@ -194,7 +194,7 @@ export function tryConsumeReplay( * Mark the (key, payload) bucket as having just observed a failed * verification. Called from the middleware's finally block when the * handler explicitly set `verifyOutcome.isValid = false` — i.e. the - * upstream store (Apple / Google / Meta) returned a definitive "this + * upstream store (Apple / Google / Horizon / Amazon) returned a definitive "this * receipt is invalid" verdict. Thrown errors from the handler (network * failures, configuration mistakes, project-not-found, etc.) do NOT * trigger the cooldown, since those aren't a verdict from the store diff --git a/packages/kit/server/api/v1/request-logger.test.ts b/packages/kit/server/api/v1/request-logger.test.ts index 6e8622f1..5e3a9c43 100644 --- a/packages/kit/server/api/v1/request-logger.test.ts +++ b/packages/kit/server/api/v1/request-logger.test.ts @@ -57,7 +57,7 @@ function buildApp(params: { return params.handler(c); } c.set("verifyOutcome", { isValid: true, state: "ENTITLED" }); - return c.json({ isValid: true, state: "ENTITLED" }); + return c.json({ store: "apple", isValid: true, state: "ENTITLED" }); }, ); return app; @@ -124,7 +124,10 @@ describe("requestLoggerMiddleware", () => { logs, handler: (c) => { c.set("verifyOutcome", { isValid: false, state: "INAUTHENTIC" }); - return c.json({ isValid: false, state: "INAUTHENTIC" }, 500); + return c.json( + { store: "google", isValid: false, state: "INAUTHENTIC" }, + 500, + ); }, }); @@ -230,7 +233,7 @@ describe("requestLoggerMiddleware", () => { validator(verifyPurchaseInputSchema), (c) => { c.set("verifyOutcome", { isValid: true, state: "ENTITLED" }); - return c.json({ isValid: true, state: "ENTITLED" }); + return c.json({ store: "apple", isValid: true, state: "ENTITLED" }); }, ); diff --git a/packages/kit/server/api/v1/route-input-schemas.ts b/packages/kit/server/api/v1/route-input-schemas.ts index a51d936c..b74383aa 100644 --- a/packages/kit/server/api/v1/route-input-schemas.ts +++ b/packages/kit/server/api/v1/route-input-schemas.ts @@ -20,7 +20,7 @@ const AMAZON_RECEIPT_ID_MAX_LENGTH = 4_096; // Lower bounds — any real token from the respective store sits well // above these. A sub-threshold input is guaranteed garbage (empty // fragment, single char, "test", etc.) and can be rejected at the -// edge so the quota counter and the upstream Apple / Google / Meta API +// edge so the quota counter and the upstream Apple / Google / Horizon / Amazon API // aren't touched. Numbers are conservative: the shortest real Apple // JWS we've observed in production is ~600 chars; the shortest Google // purchaseToken ~30; Meta userIds are 15-20 digit numeric strings. diff --git a/packages/kit/server/api/v1/route-response-schemas.test.ts b/packages/kit/server/api/v1/route-response-schemas.test.ts new file mode 100644 index 00000000..4cf100c9 --- /dev/null +++ b/packages/kit/server/api/v1/route-response-schemas.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from "vitest"; +import * as v from "valibot"; + +import { verifyPurchaseSuccessResponseSchema } from "./route-response-schemas"; + +function parse(input: unknown) { + return v.safeParse(verifyPurchaseSuccessResponseSchema, input); +} + +describe("verifyPurchaseSuccessResponseSchema", () => { + test("requires the verified store in successful responses", () => { + const result = parse({ + store: "amazon", + isValid: true, + state: "ENTITLED", + }); + + expect(result.success).toBe(true); + }); + + test("rejects legacy responses without store", () => { + const result = parse({ + isValid: true, + state: "ENTITLED", + }); + + expect(result.success).toBe(false); + }); + + test("rejects unknown stores", () => { + const result = parse({ + store: "play-store", + isValid: true, + state: "ENTITLED", + }); + + expect(result.success).toBe(false); + }); +}); diff --git a/packages/kit/server/api/v1/route-response-schemas.ts b/packages/kit/server/api/v1/route-response-schemas.ts index 7ebc872a..407801e8 100644 --- a/packages/kit/server/api/v1/route-response-schemas.ts +++ b/packages/kit/server/api/v1/route-response-schemas.ts @@ -47,7 +47,7 @@ const unifiedPurchaseStates = [ }, { name: "INAUTHENTIC", - description: "Purchase not found in App Store / Google Play.", + description: "Purchase not found in the upstream store.", }, ] as const; @@ -57,7 +57,15 @@ const unifiedPurchaseStateSchema = v.union( ), ); +const verifyStoreSchema = v.union([ + v.literal("apple"), + v.literal("google"), + v.literal("horizon"), + v.literal("amazon"), +]); + const baseReceiptResponseSchema = v.object({ + store: verifyStoreSchema, isValid: v.boolean(), state: unifiedPurchaseStateSchema, productId: v.optional( diff --git a/packages/kit/server/api/v1/routes.test.ts b/packages/kit/server/api/v1/routes.test.ts index 3f5382a8..ca8f93c5 100644 --- a/packages/kit/server/api/v1/routes.test.ts +++ b/packages/kit/server/api/v1/routes.test.ts @@ -60,6 +60,7 @@ describe("apiRoutes", () => { expect(response.status).toBe(200); expect(await response.json()).toEqual({ + store: "google", isValid: true, state: "ENTITLED", productId: "premium.monthly", diff --git a/packages/kit/server/api/v1/routes.ts b/packages/kit/server/api/v1/routes.ts index 21022e7c..9092b72b 100644 --- a/packages/kit/server/api/v1/routes.ts +++ b/packages/kit/server/api/v1/routes.ts @@ -365,6 +365,14 @@ const verifyPurchaseHandler = async ( const setOutcome = (outcome: V1AppVariables["verifyOutcome"]) => { c.set("verifyOutcome", outcome); }; + const sendReceiptResponse = ( + store: VerifyPurchaseJson["store"], + receipt: { isValid: boolean; state: string; productId?: string }, + ) => { + const outcome = { isValid: receipt.isValid, state: receipt.state }; + setOutcome(outcome); + return c.json({ store, ...receipt }); + }; try { switch (json.store) { @@ -379,8 +387,7 @@ const verifyPurchaseHandler = async ( }, ); - setOutcome({ isValid: apple.isValid, state: apple.state }); - return c.json(apple); + return sendReceiptResponse("apple", apple); } case "google": { const google = await client.action( @@ -393,8 +400,7 @@ const verifyPurchaseHandler = async ( }, ); - setOutcome({ isValid: google.isValid, state: google.state }); - return c.json(google); + return sendReceiptResponse("google", google); } case "horizon": { // Meta Horizon (Quest): the client doesn't hold a @@ -412,8 +418,7 @@ const verifyPurchaseHandler = async ( }, ); - setOutcome({ isValid: horizon.isValid, state: horizon.state }); - return c.json(horizon); + return sendReceiptResponse("horizon", horizon); } case "amazon": { const amazon = await client.action( @@ -427,8 +432,7 @@ const verifyPurchaseHandler = async ( }, ); - setOutcome({ isValid: amazon.isValid, state: amazon.state }); - return c.json(amazon); + return sendReceiptResponse("amazon", amazon); } } } catch (error) { diff --git a/packages/kit/src/pages/auth/organization/project/settings.tsx b/packages/kit/src/pages/auth/organization/project/settings.tsx index 79d2b381..022d5f67 100644 --- a/packages/kit/src/pages/auth/organization/project/settings.tsx +++ b/packages/kit/src/pages/auth/organization/project/settings.tsx @@ -1882,6 +1882,12 @@ export default function ProjectSettings() { onChange={(e) => setAmazonSharedSecret(e.target.value) } + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + void handleAmazonSubmit(); + } + }} className="w-full px-3 py-2 rounded-lg border border-border bg-background text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary" /> )} diff --git a/packages/kit/src/pages/blog/iapkit-joins-openiap.tsx b/packages/kit/src/pages/blog/iapkit-joins-openiap.tsx index 44f13553..cfbbdda9 100644 --- a/packages/kit/src/pages/blog/iapkit-joins-openiap.tsx +++ b/packages/kit/src/pages/blog/iapkit-joins-openiap.tsx @@ -28,7 +28,7 @@ const FAQ: Array<{ q: string; a: string }> = [ }, { q: "Which platforms does IAPKit support?", - a: "IAPKit validates receipts for Apple App Store, Google Play, and Meta Horizon. Fire OS and Vega OS receipt-validation support is on the roadmap.", + a: "IAPKit validates receipts for Apple App Store, Google Play, Meta Horizon, and Amazon Appstore. Vega OS receipt-validation support is on the roadmap.", }, ]; @@ -125,12 +125,12 @@ export default function IapkitJoinsOpenIap() {

    When we started building IAPKit, we kept seeing the same wall stop - developers everywhere. Apple, Google, and more recently Meta Horizon — - each platform makes in-app purchase validation harder than it needs to - be, and each in its own way. Different APIs, different edge cases, - different ways to fail. Every IAP developer reinventing the same - wheel. And with new platforms like Fire OS and Vega OS - joining the ecosystem, the fragmentation is only getting worse. + developers everywhere. Apple, Google, Meta Horizon, and Amazon each + make in-app purchase validation harder than it needs to be, and each + in its own way. Different APIs, different edge cases, different ways + to fail. Every IAP developer reinventing the same wheel. And with new + platforms like Vega OS joining the ecosystem, the fragmentation is + only getting worse.

    diff --git a/packages/kit/src/pages/docs/sections/api.tsx b/packages/kit/src/pages/docs/sections/api.tsx index 869f4f31..ea0de024 100644 --- a/packages/kit/src/pages/docs/sections/api.tsx +++ b/packages/kit/src/pages/docs/sections/api.tsx @@ -115,6 +115,7 @@ export default function ApiReferencePage() {

    Success response

    {`{ + "store": "amazon", "isValid": true, "state": "ENTITLED", "productId": "premium_monthly" @@ -245,7 +246,7 @@ export default function ApiReferencePage() { 200 - {`{ isValid, state, productId? }`} + {`{ store, isValid, state, productId? }`} Verification completed. diff --git a/packages/kit/src/pages/docs/sections/quickstart.tsx b/packages/kit/src/pages/docs/sections/quickstart.tsx index 9046ed2d..49920739 100644 --- a/packages/kit/src/pages/docs/sections/quickstart.tsx +++ b/packages/kit/src/pages/docs/sections/quickstart.tsx @@ -124,7 +124,7 @@ export default function QuickstartPage() {

    From your app, send the receipt to IAPKit. Here's the raw HTTP shape for - each of the three stores: + each supported store:

    @@ -175,6 +175,7 @@ export default function QuickstartPage() {

    Expected response:

    {`{ + "store": "amazon", "isValid": true, "state": "ENTITLED", "productId": "premium_monthly" From 38e4755fa2836bca24f8a50bee0a78eb60fe2325 Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 25 May 2026 03:24:02 +0900 Subject: [PATCH 04/51] fix(vega): address remaining review feedback Allow Vega runtime guards on supported Expo APIs, retain listeners across disconnects, bound RN purchase update pagination, add IAPKit fetch timeouts, and allow clearing Amazon RVS credentials. --- libraries/expo-iap/src/index.ts | 13 +- libraries/expo-iap/src/vega-adapter.ts | 1 - libraries/expo-iap/src/vega.kepler.ts | 6 + .../flutter_inapp_purchase/example/README.md | 1 + .../src/__tests__/vega-adapter.test.ts | 229 ++++++++++++++++++ .../react-native-iap/src/vega-adapter.ts | 152 +++++++++--- packages/docs/src/pages/docs/setup/expo.tsx | 2 +- packages/kit/convex/projects/mutation.ts | 9 +- .../auth/organization/project/settings.tsx | 56 ++++- 9 files changed, 413 insertions(+), 56 deletions(-) diff --git a/libraries/expo-iap/src/index.ts b/libraries/expo-iap/src/index.ts index 6852d608..d3be1577 100644 --- a/libraries/expo-iap/src/index.ts +++ b/libraries/expo-iap/src/index.ts @@ -106,6 +106,9 @@ type NativePurchaseUpdatedOptionsModule = { const isStorePlatform = (): boolean => Platform.OS === 'ios' || Platform.OS === 'android'; +const isStoreRuntime = (): boolean => + Platform.OS === 'ios' || isAndroidStoreRuntime(); + const unsupportedPlatformError = (): Error => new Error(`Unsupported platform: ${Platform.OS}`); @@ -578,7 +581,9 @@ export const subscriptionBillingIssueListener = ( * * @see {@link https://openiap.dev/docs/apis/init-connection} */ -export const initConnection: MutationField<'initConnection'> = async (config) => { +export const initConnection: MutationField<'initConnection'> = async ( + config, +) => { const result = await ExpoIapModule.initConnection(config ?? null); if ( result === true && @@ -780,7 +785,7 @@ export const getAvailablePurchases: QueryField< export const getActiveSubscriptions: QueryField< 'getActiveSubscriptions' > = async (subscriptionIds) => { - if (!isStorePlatform()) { + if (!isStoreRuntime()) { throw unsupportedPlatformError(); } @@ -810,7 +815,7 @@ export const getActiveSubscriptions: QueryField< export const hasActiveSubscriptions: QueryField< 'hasActiveSubscriptions' > = async (subscriptionIds) => { - if (!isStorePlatform()) { + if (!isStoreRuntime()) { throw unsupportedPlatformError(); } @@ -1272,7 +1277,7 @@ export const verifyPurchase: MutationField<'verifyPurchase'> = async ( export const verifyPurchaseWithProvider: MutationField< 'verifyPurchaseWithProvider' > = async (options) => { - if (!isStorePlatform()) { + if (!isStoreRuntime()) { throw unsupportedPlatformError(); } diff --git a/libraries/expo-iap/src/vega-adapter.ts b/libraries/expo-iap/src/vega-adapter.ts index 1bf63de7..b16dcf30 100644 --- a/libraries/expo-iap/src/vega-adapter.ts +++ b/libraries/expo-iap/src/vega-adapter.ts @@ -776,7 +776,6 @@ export function createExpoIapVegaModule( async endConnection(): Promise { productTypesBySku.clear(); cachedUserData = null; - listenersByEventName.clear(); return true; }, async fetchProducts( diff --git a/libraries/expo-iap/src/vega.kepler.ts b/libraries/expo-iap/src/vega.kepler.ts index a07894fb..c714711b 100644 --- a/libraries/expo-iap/src/vega.kepler.ts +++ b/libraries/expo-iap/src/vega.kepler.ts @@ -9,10 +9,16 @@ import { let cachedVegaModule: ExpoIapVegaModule | null = null; +/** + * Returns true when the current React Native platform is Amazon Vega/Kepler. + */ export const isVegaOS = (): boolean => { return String(Platform.OS).toLowerCase() === 'kepler'; }; +/** + * Lazily creates the Vega IAP adapter backed by Amazon's Kepler Appstore IAP service. + */ export const getVegaIapModule = (): ExpoIapVegaModule | null => { if (!isVegaOS()) return null; if (!cachedVegaModule) { diff --git a/libraries/flutter_inapp_purchase/example/README.md b/libraries/flutter_inapp_purchase/example/README.md index cdcfc250..223ae992 100644 --- a/libraries/flutter_inapp_purchase/example/README.md +++ b/libraries/flutter_inapp_purchase/example/README.md @@ -70,6 +70,7 @@ To use Fire OS IAP through the Amazon Appstore SDK: ``` 3. **Test with Amazon App Tester** on a Fire OS or compatible Android test device: + ```bash flutter run flutter build apk --release diff --git a/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts b/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts index 5c359e30..0b07f2d2 100644 --- a/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts +++ b/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts @@ -422,6 +422,28 @@ describe('Amazon Vega adapter', () => { } }); + it('rejects mixed IAPKit payloads on the Amazon Vega adapter', async () => { + const service = createService(); + const module = createVegaIapModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + google: { + purchaseToken: 'google-token', + }, + }, + }), + ).rejects.toThrow( + `"message":"Amazon Vega IAPKit verification requires exactly one amazon payload."`, + ); + }); + it('wraps non-JSON IAPKit failures as receipt errors', async () => { const service = createService(); const originalFetch = globalThis.fetch; @@ -450,6 +472,41 @@ describe('Amazon Vega adapter', () => { } }); + it('extracts nested JSON IAPKit failure messages', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + Response.json( + { + message: JSON.stringify({ + error: 'receipt no longer valid', + }), + }, + {status: 400}, + ), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createVegaIapModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toThrow(`"message":"receipt no longer valid"`); + } finally { + globalThis.fetch = originalFetch; + } + }); + it('rejects empty successful IAPKit responses as receipt errors', async () => { const service = createService(); const originalFetch = globalThis.fetch; @@ -480,6 +537,147 @@ describe('Amazon Vega adapter', () => { } }); + it('treats successful IAPKit error payloads as receipt errors', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + Response.json( + { + errors: [ + { + code: 'BAD_RECEIPT', + message: 'bad receipt', + }, + ], + }, + {status: 200}, + ), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createVegaIapModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toThrow(`"message":"bad receipt"`); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('rejects malformed successful IAPKit payloads', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + Response.json(['not', 'an', 'object'], {status: 200}), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createVegaIapModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toThrow( + `"message":"IAPKit returned malformed response (HTTP 200)."`, + ); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('rejects successful IAPKit payloads missing required fields', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + Response.json( + { + state: 'ENTITLED', + store: 'amazon', + }, + {status: 200}, + ), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createVegaIapModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toThrow( + `"message":"IAPKit returned malformed response (HTTP 200)."`, + ); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('rejects successful IAPKit payloads for another store', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + Response.json( + { + isValid: true, + state: 'ENTITLED', + store: 'apple', + }, + {status: 200}, + ), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createVegaIapModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toThrow( + `"message":"IAPKit returned malformed response (HTTP 200)."`, + ); + } finally { + globalThis.fetch = originalFetch; + } + }); + it('wraps IAPKit network failures as network errors', async () => { const service = createService(); const originalFetch = globalThis.fetch; @@ -506,4 +704,35 @@ describe('Amazon Vega adapter', () => { globalThis.fetch = originalFetch; } }); + + it('wraps IAPKit response body read failures as network errors', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn(async () => ({ + ok: true, + status: 200, + text: async () => { + throw new TypeError('body stream failed'); + }, + })) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createVegaIapModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toThrow(`"code":"network-error"`); + } finally { + globalThis.fetch = originalFetch; + } + }); }); diff --git a/libraries/react-native-iap/src/vega-adapter.ts b/libraries/react-native-iap/src/vega-adapter.ts index 11d7b0a3..7861b43e 100644 --- a/libraries/react-native-iap/src/vega-adapter.ts +++ b/libraries/react-native-iap/src/vega-adapter.ts @@ -17,6 +17,9 @@ type ResponseOperation = | 'user-data' | 'notify-fulfillment'; +const IAPKIT_VERIFY_TIMEOUT_MS = 10_000; +const MAX_PURCHASE_UPDATE_PAGES = 100; + interface VegaPrice { priceCurrencyCode?: string | null; priceStr?: string | null; @@ -148,10 +151,6 @@ function toPurchaseErrorResult( }; } -function getResponseCode(response?: VegaResponse | null): unknown { - return response?.responseCode; -} - function responseCodeName(responseCode: unknown): string { if (typeof responseCode === 'string') { return responseCode.toUpperCase(); @@ -209,7 +208,7 @@ function ensureSuccessful( message: string, productId?: string, ): void { - const responseCode = getResponseCode(response); + const responseCode = response?.responseCode; if (isSuccess(operation, responseCode)) return; throw createVegaError( @@ -447,8 +446,17 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { const receipts: VegaReceipt[] = []; let reset = true; let hasMore = false; + let pageCount = 0; do { + if (pageCount >= MAX_PURCHASE_UPDATE_PAGES) { + throw createVegaError( + ErrorCode.ServiceError, + 'Amazon Vega purchase updates exceeded the pagination limit.', + ); + } + pageCount++; + const response = await service.getPurchaseUpdates({reset}); ensureSuccessful( 'purchase-updates', @@ -506,6 +514,7 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { await hydrateProductTypesForReceipts(receipts); return receipts .filter((receipt) => { + if (receipt.isCancelled || receipt.cancelDate) return false; if (!includeSuspended && receipt.isDeferred) return false; const openIapType = productTypeToOpenIap( receipt.productType ?? @@ -574,19 +583,23 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { function extractIapkitErrorMessage(json: unknown): string | null { if (!json || typeof json !== 'object') return null; const record = json as Record; + function extractStringMessage(value: string): string { + try { + const parsed = JSON.parse(value); + return parsed && typeof parsed === 'object' + ? (extractIapkitErrorMessage(parsed) ?? value) + : value; + } catch { + return value; + } + } + const details = record.details; if (details && typeof details === 'object') { const originalError = (details as Record) .originalError; if (typeof originalError === 'string') { - try { - return ( - extractIapkitErrorMessage(JSON.parse(originalError)) ?? - originalError - ); - } catch { - return originalError; - } + return extractStringMessage(originalError); } } @@ -595,11 +608,13 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { return extractIapkitErrorMessage(errors[0]); } - return typeof record.message === 'string' - ? record.message - : typeof record.error === 'string' - ? record.error - : null; + if (typeof record.message === 'string') { + return extractStringMessage(record.message); + } + if (typeof record.error === 'string') { + return extractStringMessage(record.error); + } + return null; } function parseIapkitJsonResponse(text: string): unknown | null { @@ -611,6 +626,42 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { } } + function isIapkitResultObject( + json: unknown, + ): json is Record { + return Boolean(json) && typeof json === 'object' && !Array.isArray(json); + } + + function hasIapkitErrors(json: unknown): boolean { + if (!isIapkitResultObject(json)) return false; + const errors = json.errors; + return Array.isArray(errors) && errors.length > 0; + } + + function readIapkitResult( + json: Record, + status: number, + ): { + isValid: boolean; + state: IapkitPurchaseState; + } { + if ( + typeof json.isValid !== 'boolean' || + typeof json.state !== 'string' || + json.store !== 'amazon' + ) { + throw createVegaError( + ErrorCode.ReceiptFailed, + `IAPKit returned malformed response (HTTP ${status}).`, + ); + } + + return { + isValid: json.isValid, + state: normalizeIapkitState(json.state), + }; + } + if (params.provider !== 'iapkit') { throw createVegaError( ErrorCode.FeatureNotSupported, @@ -619,15 +670,20 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { } const iapkit = params.iapkit; + const payloadCount = + Number(Boolean(iapkit?.amazon)) + + Number(Boolean(iapkit?.apple)) + + Number(Boolean(iapkit?.google)); const amazon = iapkit?.amazon; - if (!amazon) { + if (payloadCount !== 1 || !amazon) { throw createVegaError( ErrorCode.DeveloperError, - 'Amazon Vega IAPKit verification requires amazon parameters.', + 'Amazon Vega IAPKit verification requires exactly one amazon payload.', ); } - const receiptId = amazon.receiptId.trim(); + const receiptId = + typeof amazon.receiptId === 'string' ? amazon.receiptId.trim() : ''; if (!receiptId) { throw createVegaError( ErrorCode.DeveloperError, @@ -635,7 +691,7 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { ); } - let userId = amazon.userId?.trim() ?? ''; + let userId = typeof amazon.userId === 'string' ? amazon.userId.trim() : ''; if (!userId) { const response = await service.getUserData({}); ensureSuccessful( @@ -653,15 +709,20 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { ); } + const apiKey = + typeof iapkit?.apiKey === 'string' ? iapkit.apiKey.trim() : ''; let response: Response; try { + const controller = new AbortController(); + const timeout = setTimeout( + () => controller.abort(), + IAPKIT_VERIFY_TIMEOUT_MS, + ); response = await fetch(IAPKIT_VERIFY_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', - ...(iapkit?.apiKey - ? {Authorization: `Bearer ${iapkit.apiKey}`} - : {}), + ...(apiKey ? {Authorization: `Bearer ${apiKey}`} : {}), }, body: JSON.stringify({ store: 'amazon', @@ -669,7 +730,8 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { receiptId, ...(amazon.sandbox == null ? {} : {sandbox: amazon.sandbox}), }), - }); + signal: controller.signal, + }).finally(() => clearTimeout(timeout)); } catch (error) { throw createVegaError( ErrorCode.NetworkError, @@ -678,7 +740,17 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { : 'Failed to reach IAPKit verification endpoint.', ); } - const text = await response.text(); + let text: string; + try { + text = await response.text(); + } catch (error) { + throw createVegaError( + ErrorCode.NetworkError, + error instanceof Error + ? error.message + : 'Failed to read IAPKit verification response.', + ); + } const json = parseIapkitJsonResponse(text); if (!response.ok) { @@ -695,16 +767,26 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { ); } - const result = json as { - isValid?: unknown; - state?: unknown; - store?: unknown; - }; + if (!isIapkitResultObject(json)) { + throw createVegaError( + ErrorCode.ReceiptFailed, + `IAPKit returned malformed response (HTTP ${response.status}).`, + ); + } + + if (hasIapkitErrors(json)) { + throw createVegaError( + ErrorCode.ReceiptFailed, + extractIapkitErrorMessage(json) ?? 'IAPKit verification failed.', + ); + } + + const result = readIapkitResult(json, response.status); return { provider: 'iapkit', iapkit: { - isValid: result.isValid === true, - state: normalizeIapkitState(result.state), + isValid: result.isValid, + state: result.state, store: 'amazon', }, }; @@ -718,8 +800,6 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { async endConnection(): Promise { productTypesBySku.clear(); cachedUserData = null; - purchaseUpdateListeners.clear(); - purchaseErrorListeners.clear(); return true; }, async fetchProducts(skus: string[], type: string): Promise { diff --git a/packages/docs/src/pages/docs/setup/expo.tsx b/packages/docs/src/pages/docs/setup/expo.tsx index f69d49d0..4c52c633 100644 --- a/packages/docs/src/pages/docs/setup/expo.tsx +++ b/packages/docs/src/pages/docs/setup/expo.tsx @@ -352,7 +352,7 @@ cd ios && pod install`} modules.vega - runtime + boolean Declares Vega OS runtime support and enables conflict checks; it does not select an Android flavor. Install Amazon's Vega IAP diff --git a/packages/kit/convex/projects/mutation.ts b/packages/kit/convex/projects/mutation.ts index 9a7c173a..34d188fa 100644 --- a/packages/kit/convex/projects/mutation.ts +++ b/packages/kit/convex/projects/mutation.ts @@ -301,7 +301,7 @@ export const updateProject = mutation({ horizonEnabled: v.optional(v.boolean()), horizonAppId: v.optional(v.string()), horizonAppSecret: v.optional(v.string()), - amazonSharedSecret: v.optional(v.string()), + amazonSharedSecret: v.optional(v.union(v.string(), v.null())), reportingCurrency: v.optional(v.string()), }, handler: async (ctx, args) => { @@ -393,9 +393,10 @@ export const updateProject = mutation({ ); } if (args.amazonSharedSecret !== undefined) { - updates.amazonSharedSecret = normalizeAmazonSharedSecret( - args.amazonSharedSecret, - ); + updates.amazonSharedSecret = + args.amazonSharedSecret === null + ? null + : normalizeAmazonSharedSecret(args.amazonSharedSecret); } // Invariant: enabling Horizon without both credentials leaves the diff --git a/packages/kit/src/pages/auth/organization/project/settings.tsx b/packages/kit/src/pages/auth/organization/project/settings.tsx index 022d5f67..76c82071 100644 --- a/packages/kit/src/pages/auth/organization/project/settings.tsx +++ b/packages/kit/src/pages/auth/organization/project/settings.tsx @@ -525,6 +525,31 @@ export default function ProjectSettings() { } }; + const handleAmazonClear = async () => { + if (!project || !hasAmazonSharedSecretConfigured || savingAmazon) { + return; + } + + setSavingAmazon(true); + try { + await updateProject({ + projectId: project._id, + amazonSharedSecret: null, + }); + + setAmazonSharedSecret(""); + setIsReplacingAmazonSharedSecret(false); + toast.success("Amazon RVS configuration removed."); + } catch (error: any) { + console.error("Amazon RVS config clear error:", error); + toast.error( + error.message || "Failed to remove Amazon RVS configuration.", + ); + } finally { + setSavingAmazon(false); + } + }; + const handleReportingCurrencySubmit = async ( event: FormEvent, ) => { @@ -1860,16 +1885,27 @@ export default function ProjectSettings() { {"Shared Secret configured"}
    - +
    + + +
  • ) : ( Date: Mon, 25 May 2026 03:34:47 +0900 Subject: [PATCH 05/51] fix(runtime): address latest review feedback --- libraries/expo-iap/src/vega-adapter.ts | 12 +++- .../react-native-iap/src/vega-adapter.ts | 12 +++- .../java/dev/hyo/openiap/OpenIapModule.kt | 58 +++++++++++-------- packages/kit/convex/purchases/amazon.test.ts | 21 +++---- packages/kit/convex/purchases/amazon.ts | 7 --- 5 files changed, 61 insertions(+), 49 deletions(-) diff --git a/libraries/expo-iap/src/vega-adapter.ts b/libraries/expo-iap/src/vega-adapter.ts index b16dcf30..68e8480e 100644 --- a/libraries/expo-iap/src/vega-adapter.ts +++ b/libraries/expo-iap/src/vega-adapter.ts @@ -18,6 +18,7 @@ type ResponseOperation = | 'notify-fulfillment'; const IAPKIT_VERIFY_TIMEOUT_MS = 10_000; +const MAX_IAPKIT_ERROR_DEPTH = 5; type VegaListener = (payload: any) => void; @@ -555,14 +556,19 @@ export function createExpoIapVegaModule( : 'unknown'; } - function extractIapkitErrorMessage(json: unknown): string | null { + function extractIapkitErrorMessage( + json: unknown, + depth = 0, + ): string | null { + if (depth > MAX_IAPKIT_ERROR_DEPTH) return null; if (!json || typeof json !== 'object') return null; const record = json as Record; function extractStringMessage(value: string): string { + if (depth >= MAX_IAPKIT_ERROR_DEPTH) return value; try { const parsed = JSON.parse(value); return parsed && typeof parsed === 'object' - ? (extractIapkitErrorMessage(parsed) ?? value) + ? (extractIapkitErrorMessage(parsed, depth + 1) ?? value) : value; } catch { return value; @@ -580,7 +586,7 @@ export function createExpoIapVegaModule( const errors = record.errors; if (Array.isArray(errors) && errors.length > 0) { - return extractIapkitErrorMessage(errors[0]); + return extractIapkitErrorMessage(errors[0], depth + 1); } if (typeof record.message === 'string') { diff --git a/libraries/react-native-iap/src/vega-adapter.ts b/libraries/react-native-iap/src/vega-adapter.ts index 7861b43e..da80184c 100644 --- a/libraries/react-native-iap/src/vega-adapter.ts +++ b/libraries/react-native-iap/src/vega-adapter.ts @@ -18,6 +18,7 @@ type ResponseOperation = | 'notify-fulfillment'; const IAPKIT_VERIFY_TIMEOUT_MS = 10_000; +const MAX_IAPKIT_ERROR_DEPTH = 5; const MAX_PURCHASE_UPDATE_PAGES = 100; interface VegaPrice { @@ -580,14 +581,19 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { : 'unknown'; } - function extractIapkitErrorMessage(json: unknown): string | null { + function extractIapkitErrorMessage( + json: unknown, + depth = 0, + ): string | null { + if (depth > MAX_IAPKIT_ERROR_DEPTH) return null; if (!json || typeof json !== 'object') return null; const record = json as Record; function extractStringMessage(value: string): string { + if (depth >= MAX_IAPKIT_ERROR_DEPTH) return value; try { const parsed = JSON.parse(value); return parsed && typeof parsed === 'object' - ? (extractIapkitErrorMessage(parsed) ?? value) + ? (extractIapkitErrorMessage(parsed, depth + 1) ?? value) : value; } catch { return value; @@ -605,7 +611,7 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { const errors = record.errors; if (Array.isArray(errors) && errors.length > 0) { - return extractIapkitErrorMessage(errors[0]); + return extractIapkitErrorMessage(errors[0], depth + 1); } if (typeof record.message === 'string') { diff --git a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt index 09369475..57622e98 100644 --- a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt @@ -311,29 +311,11 @@ class OpenIapModule( } override val acknowledgePurchaseAndroid: MutationAcknowledgePurchaseAndroidHandler = { purchaseToken -> - withContext(Dispatchers.IO) { - runCatching { - ensureRegistered() - PurchasingService.notifyFulfillment(purchaseToken, FulfillmentResult.FULFILLED) - true - }.getOrElse { - OpenIapLog.w("Amazon acknowledge failed: ${it.message}", TAG) - false - } - } + acknowledgePurchase(purchaseToken) } override val consumePurchaseAndroid: MutationConsumePurchaseAndroidHandler = { purchaseToken -> - withContext(Dispatchers.IO) { - runCatching { - ensureRegistered() - PurchasingService.notifyFulfillment(purchaseToken, FulfillmentResult.FULFILLED) - true - }.getOrElse { - OpenIapLog.w("Amazon consume failed: ${it.message}", TAG) - false - } - } + consumePurchase(purchaseToken) } override val restorePurchases: MutationRestorePurchasesHandler = { @@ -673,6 +655,30 @@ class OpenIapModule( ?: storefrontCode } + private suspend fun acknowledgePurchase(purchaseToken: String): Boolean = fulfillPurchase( + purchaseToken = purchaseToken, + operation = "acknowledge" + ) + + private suspend fun consumePurchase(purchaseToken: String): Boolean = fulfillPurchase( + purchaseToken = purchaseToken, + operation = "consume" + ) + + private suspend fun fulfillPurchase( + purchaseToken: String, + operation: String + ): Boolean = withContext(Dispatchers.IO) { + runCatching { + ensureRegistered() + PurchasingService.notifyFulfillment(purchaseToken, FulfillmentResult.FULFILLED) + true + }.getOrElse { + OpenIapLog.w("Amazon $operation failed: ${it.message}", TAG) + false + } + } + private fun AmazonProduct.toInAppProduct(): ProductAndroid { return ProductAndroid( currency = "", @@ -744,6 +750,7 @@ class OpenIapModule( return when (value.lowercase(Locale.ROOT)) { "weekly", "week", "1 week" -> "P1W" "monthly", "month", "1 month" -> "P1M" + "bi-monthly", "bimonthly", "2 month", "2 months" -> "P2M" "quarterly", "quarter", "3 months" -> "P3M" "semiannual", "semiannually", "semi-annual", "semi-annually", "6 months" -> "P6M" "annual", "annually", "yearly", "year", "1 year" -> "P1Y" @@ -758,10 +765,15 @@ class OpenIapModule( val numeric = value.replace(Regex("[^0-9,.-]"), "") if (numeric.isBlank()) return 0.0 - val normalized = if (numeric.contains(',') && !numeric.contains('.')) { - numeric.replace(',', '.') + val lastDot = numeric.lastIndexOf('.') + val lastComma = numeric.lastIndexOf(',') + val decimalIndex = maxOf(lastDot, lastComma) + val normalized = if (decimalIndex >= 0) { + val integerPart = numeric.substring(0, decimalIndex).replace(Regex("[^0-9-]"), "") + val fractionPart = numeric.substring(decimalIndex + 1).replace(Regex("[^0-9]"), "") + "$integerPart.$fractionPart" } else { - numeric.replace(",", "") + numeric.replace(Regex("[^0-9-]"), "") } return normalized.toDoubleOrNull() ?: 0.0 } diff --git a/packages/kit/convex/purchases/amazon.test.ts b/packages/kit/convex/purchases/amazon.test.ts index 4f1bc2ca..b8081cef 100644 --- a/packages/kit/convex/purchases/amazon.test.ts +++ b/packages/kit/convex/purchases/amazon.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test, vi } from "vitest"; +import { describe, expect, test } from "vitest"; import { buildAmazonRemoteId, @@ -50,18 +50,13 @@ describe("mapAmazonReceiptState", () => { ); }); - test("maps expired subscription renewalDate to expired", () => { - const nowSpy = vi.spyOn(Date, "now").mockReturnValue(2_000); - try { - expect( - mapAmazonReceiptState({ - productType: "SUBSCRIPTION", - renewalDate: 1_000, - }), - ).toBe(HarmonizedPurchaseState.EXPIRED); - } finally { - nowSpy.mockRestore(); - } + test("does not treat Amazon subscription renewalDate as expiry", () => { + expect( + mapAmazonReceiptState({ + productType: "SUBSCRIPTION", + renewalDate: 1_000, + }), + ).toBe(HarmonizedPurchaseState.ENTITLED); }); test("falls back to unknown for unrecognized product types", () => { diff --git a/packages/kit/convex/purchases/amazon.ts b/packages/kit/convex/purchases/amazon.ts index f159a2aa..43c2b3de 100644 --- a/packages/kit/convex/purchases/amazon.ts +++ b/packages/kit/convex/purchases/amazon.ts @@ -105,13 +105,6 @@ export function mapAmazonReceiptState( case "ENTITLED": return HarmonizedPurchaseState.ENTITLED; case "SUBSCRIPTION": - if ( - receipt.renewalDate !== undefined && - receipt.renewalDate !== null && - receipt.renewalDate < Date.now() - ) { - return HarmonizedPurchaseState.EXPIRED; - } return HarmonizedPurchaseState.ENTITLED; default: return HarmonizedPurchaseState.UNKNOWN; From bd30736b05f9e7ab8733df58aeee4133787cc863 Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 25 May 2026 03:38:26 +0900 Subject: [PATCH 06/51] fix(amazon): correct fire os purchase mapping --- .../openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt index 57622e98..68bea786 100644 --- a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt @@ -83,8 +83,6 @@ class OpenIapModule( private fun ensureRegistered() { if (isRegistered) return PurchasingService.registerListener(context.applicationContext, this) - runCatching { PurchasingService.enablePendingPurchases() } - .onFailure { OpenIapLog.w("Amazon pending purchases unavailable: ${it.message}", TAG) } isRegistered = true } @@ -749,6 +747,7 @@ class OpenIapModule( return when (value.lowercase(Locale.ROOT)) { "weekly", "week", "1 week" -> "P1W" + "biweekly", "bi-weekly", "bi weekly", "2 week", "2 weeks" -> "P2W" "monthly", "month", "1 month" -> "P1M" "bi-monthly", "bimonthly", "2 month", "2 months" -> "P2M" "quarterly", "quarter", "3 months" -> "P3M" From 75660a14aaf2acb41774542e5218449be26ce775 Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 25 May 2026 03:54:22 +0900 Subject: [PATCH 07/51] fix(amazon): harden fire os purchase flow --- .../java/dev/hyo/openiap/OpenIapModule.kt | 81 +++++++++++++------ 1 file changed, 55 insertions(+), 26 deletions(-) diff --git a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt index 68bea786..a6d5a498 100644 --- a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt @@ -38,13 +38,14 @@ import com.amazon.device.iap.model.Receipt as AmazonReceipt private const val TAG = "OpenIapAmazon" private const val AMAZON_REQUEST_TIMEOUT_MS = 60_000L private const val AMAZON_PRODUCT_DATA_BATCH_SIZE = 100 +private const val AMAZON_PURCHASE_UPDATES_MAX_PAGES = 100 /** * OpenIapModule for Amazon Appstore SDK IAP. * * Amazon's native IAP API is listener based instead of connection based. The - * OpenIAP connection lifecycle registers the listener and enables pending - * purchases, while individual API calls await the matching RequestId callback. + * OpenIAP connection lifecycle registers the listener, while individual API + * calls await the matching RequestId callback. */ class OpenIapModule( private val context: Context, @@ -228,23 +229,27 @@ class OpenIapModule( ensureRegistered() val androidArgs = props.toAndroidPurchaseArgs() if (androidArgs.skus.isEmpty()) { - emitPurchaseError(OpenIapError.EmptySkuList) - return@withContext emptyList() + emitPurchaseErrorAndThrow(OpenIapError.EmptySkuList) } if (androidArgs.skus.size != 1) { - emitPurchaseError( + emitPurchaseErrorAndThrow( OpenIapError.DeveloperError("Amazon Appstore SDK purchases one SKU at a time") ) - return@withContext emptyList() } val sku = androidArgs.skus.first() - val response = requestAmazonPurchase(sku) + val response = runCatching { requestAmazonPurchase(sku) } + .getOrElse { error -> + emitPurchaseErrorAndThrow( + error.toOpenIapError("Amazon purchase request failed") + ) + } when (response.requestStatus) { PurchaseResponse.RequestStatus.SUCCESSFUL -> { val receipt = response.receipt ?: run { - emitPurchaseError(OpenIapError.PurchaseFailed("Amazon purchase response did not include a receipt")) - return@withContext emptyList() + emitPurchaseErrorAndThrow( + OpenIapError.PurchaseFailed("Amazon purchase response did not include a receipt") + ) } val purchase = receipt.toPurchase() purchaseTypeByReceiptId[receipt.receiptId] = receipt.productType @@ -256,28 +261,23 @@ class OpenIapModule( } PurchaseResponse.RequestStatus.ALREADY_PURCHASED -> { val error = OpenIapError.ItemAlreadyOwned("Amazon reported the item is already purchased") - emitPurchaseError(error) - emptyList() + emitPurchaseErrorAndThrow(error) } PurchaseResponse.RequestStatus.INVALID_SKU -> { val error = OpenIapError.SkuNotFound(sku) - emitPurchaseError(error) - emptyList() + emitPurchaseErrorAndThrow(error) } PurchaseResponse.RequestStatus.NOT_SUPPORTED -> { val error = OpenIapError.FeatureNotSupported("Amazon Appstore IAP is not supported on this device") - emitPurchaseError(error) - emptyList() + emitPurchaseErrorAndThrow(error) } PurchaseResponse.RequestStatus.PENDING -> { val error = OpenIapError.PurchaseDeferred - emitPurchaseError(error) - emptyList() + emitPurchaseErrorAndThrow(error) } PurchaseResponse.RequestStatus.FAILED -> { val error = OpenIapError.UserCancelled("Amazon purchase failed or was cancelled") - emitPurchaseError(error) - emptyList() + emitPurchaseErrorAndThrow(error) } } } @@ -549,6 +549,11 @@ class OpenIapModule( } } + private fun emitPurchaseErrorAndThrow(error: OpenIapError): Nothing { + emitPurchaseError(error) + throw error + } + private suspend fun requestUserData(): UserDataResponse { val requestId = withContext(Dispatchers.Main) { ensureRegistered() @@ -568,7 +573,14 @@ class OpenIapModule( private suspend fun requestAmazonPurchase(sku: String): PurchaseResponse { val requestId = withContext(Dispatchers.Main) { ensureRegistered() - PurchasingService.purchase(sku).toString() + val request = runCatching { PurchasingService.purchase(sku) } + .getOrElse { error -> + throw error.toOpenIapError("Amazon purchase request failed") + } + if (request == null) { + throw OpenIapError.PurchaseFailed("Amazon purchase request did not return a requestId") + } + request.toString() } return awaitAmazonResponse(requestId, purchaseRequests, earlyPurchaseResponses) } @@ -576,13 +588,19 @@ class OpenIapModule( private suspend fun requestPurchaseUpdates(reset: Boolean): List { val purchases = mutableListOf() var shouldReset = reset + var pageCount = 0 do { + if (pageCount >= AMAZON_PURCHASE_UPDATES_MAX_PAGES) { + OpenIapLog.w("Amazon purchase updates exceeded pagination limit", TAG) + break + } + pageCount += 1 val response = awaitPurchaseUpdates(shouldReset) shouldReset = false when (response.requestStatus) { PurchaseUpdatesResponse.RequestStatus.SUCCESSFUL -> { purchases += response.receipts.orEmpty() - .filter { !it.isCanceled } + .filter { it.cancelDate == null } .map { receipt -> purchaseTypeByReceiptId[receipt.receiptId] = receipt.productType productTypeBySku[receipt.sku] = receipt.productType @@ -600,6 +618,11 @@ class OpenIapModule( return purchases } + private fun Throwable.toOpenIapError(defaultMessage: String): OpenIapError { + return this as? OpenIapError + ?: OpenIapError.PurchaseFailed("$defaultMessage: ${message ?: javaClass.simpleName}") + } + private suspend fun awaitPurchaseUpdates(reset: Boolean): PurchaseUpdatesResponse { val requestId = withContext(Dispatchers.Main) { ensureRegistered() @@ -686,7 +709,7 @@ class OpenIapModule( id = sku, nameAndroid = title.orEmpty(), platform = IapPlatform.Android, - price = null, + price = price.toPriceAmount(), productStatusAndroid = ProductStatusAndroid.Ok, title = title.orEmpty(), type = ProductType.InApp @@ -695,6 +718,7 @@ class OpenIapModule( private fun AmazonProduct.toSubscriptionProduct(): ProductSubscriptionAndroid { val subscriptionPeriod = this.subscriptionPeriod.toIsoBillingPeriod() + val priceAmount = price.toPriceAmount() val phase = PricingPhaseAndroid( billingCycleCount = 0, billingPeriod = subscriptionPeriod, @@ -720,7 +744,7 @@ class OpenIapModule( offerTokenAndroid = "", paymentMode = PaymentMode.PayAsYouGo, period = null, - price = price.toPriceAmount(), + price = priceAmount, pricingPhasesAndroid = phases, type = DiscountOfferType.Introductory ) @@ -732,7 +756,7 @@ class OpenIapModule( id = sku, nameAndroid = title.orEmpty(), platform = IapPlatform.Android, - price = null, + price = priceAmount, productStatusAndroid = ProductStatusAndroid.Ok, subscriptionOfferDetailsAndroid = listOf(legacyOffer), subscriptionOffers = listOf(standardizedOffer), @@ -767,7 +791,10 @@ class OpenIapModule( val lastDot = numeric.lastIndexOf('.') val lastComma = numeric.lastIndexOf(',') val decimalIndex = maxOf(lastDot, lastComma) - val normalized = if (decimalIndex >= 0) { + val hasMixedSeparators = lastDot >= 0 && lastComma >= 0 + val fractionLength = if (decimalIndex >= 0) numeric.length - decimalIndex - 1 else 0 + val hasDecimalSeparator = decimalIndex >= 0 && (hasMixedSeparators || fractionLength in 1..2) + val normalized = if (hasDecimalSeparator) { val integerPart = numeric.substring(0, decimalIndex).replace(Regex("[^0-9-]"), "") val fractionPart = numeric.substring(decimalIndex + 1).replace(Regex("[^0-9]"), "") "$integerPart.$fractionPart" @@ -780,6 +807,7 @@ class OpenIapModule( private fun AmazonReceipt.toPurchase(): PurchaseAndroid { val isSubscription = productType == AmazonProductType.SUBSCRIPTION val dateMillis = purchaseDate?.time?.toDouble() ?: 0.0 + val isCanceled = cancelDate != null val state = if (isCanceled) PurchaseState.Unknown else PurchaseState.Purchased return PurchaseAndroid( autoRenewingAndroid = isSubscription && !isCanceled, @@ -798,7 +826,8 @@ class OpenIapModule( signatureAndroid = null, store = IapStore.Amazon, transactionDate = dateMillis, - transactionId = receiptId + transactionId = receiptId, + isSuspendedAndroid = false ) } From ea558e27966f6df64859f7525d83b796cdb412ba Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 25 May 2026 06:55:34 +0900 Subject: [PATCH 08/51] fix(amazon): parallelize fire os product fetch --- .../java/dev/hyo/openiap/OpenIapModule.kt | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt index a6d5a498..8f616ca8 100644 --- a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt @@ -26,6 +26,9 @@ import dev.hyo.openiap.utils.verifyPurchaseWithIapkit import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import java.lang.ref.WeakReference @@ -140,7 +143,13 @@ class OpenIapModule( val responses = params.skus .chunked(AMAZON_PRODUCT_DATA_BATCH_SIZE) - .map { requestProductData(it) } + .let { batches -> + coroutineScope { + batches.map { batch -> + async { requestProductData(batch) } + }.awaitAll() + } + } val failedResponse = responses.firstOrNull { it.requestStatus != ProductDataResponse.RequestStatus.SUCCESSFUL } @@ -189,10 +198,15 @@ class OpenIapModule( } } - override val getAvailablePurchases: QueryGetAvailablePurchasesHandler = { + override val getAvailablePurchases: QueryGetAvailablePurchasesHandler = { options -> withContext(Dispatchers.IO) { ensureRegistered() - requestPurchaseUpdates(reset = true) + val purchases = requestPurchaseUpdates(reset = true) + if (options?.includeSuspendedAndroid == true) { + purchases + } else { + purchases.filterNot { (it as? PurchaseAndroid)?.isSuspendedAndroid == true } + } } } From 34f1682c071e0fdd36710dc7a32b9dc92a4567d2 Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 25 May 2026 08:51:37 +0900 Subject: [PATCH 09/51] fix(runtime): handle review edge cases --- .../src/__tests__/vega-adapter.test.ts | 36 +++++++++++++++++++ libraries/expo-iap/src/vega-adapter.ts | 5 ++- .../src/__tests__/vega-adapter.test.ts | 33 +++++++++++++++++ .../react-native-iap/src/vega-adapter.ts | 5 ++- .../java/dev/hyo/openiap/OpenIapModule.kt | 5 +-- 5 files changed, 80 insertions(+), 4 deletions(-) diff --git a/libraries/expo-iap/src/__tests__/vega-adapter.test.ts b/libraries/expo-iap/src/__tests__/vega-adapter.test.ts index 7c2d8cb0..6067a5e0 100644 --- a/libraries/expo-iap/src/__tests__/vega-adapter.test.ts +++ b/libraries/expo-iap/src/__tests__/vega-adapter.test.ts @@ -508,6 +508,42 @@ describe('Amazon Vega Expo adapter', () => { } }); + it('extracts string entries from IAPKit error arrays', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + Response.json( + { + errors: ['receipt array failure'], + }, + {status: 400}, + ), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createExpoIapVegaModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toMatchObject({ + code: ErrorCode.ReceiptFailed, + message: 'receipt array failure', + }); + } finally { + globalThis.fetch = originalFetch; + } + }); + it('rejects empty successful IAPKit responses as receipt errors', async () => { const service = createService(); const originalFetch = globalThis.fetch; diff --git a/libraries/expo-iap/src/vega-adapter.ts b/libraries/expo-iap/src/vega-adapter.ts index 68e8480e..02b491e3 100644 --- a/libraries/expo-iap/src/vega-adapter.ts +++ b/libraries/expo-iap/src/vega-adapter.ts @@ -586,7 +586,10 @@ export function createExpoIapVegaModule( const errors = record.errors; if (Array.isArray(errors) && errors.length > 0) { - return extractIapkitErrorMessage(errors[0], depth + 1); + const firstError = errors[0]; + return typeof firstError === 'string' + ? extractStringMessage(firstError) + : extractIapkitErrorMessage(firstError, depth + 1); } if (typeof record.message === 'string') { diff --git a/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts b/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts index 0b07f2d2..998a0b3e 100644 --- a/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts +++ b/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts @@ -507,6 +507,39 @@ describe('Amazon Vega adapter', () => { } }); + it('extracts string entries from IAPKit error arrays', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + Response.json( + { + errors: ['receipt array failure'], + }, + {status: 400}, + ), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createVegaIapModule(service); + + await expect( + module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + }), + ).rejects.toThrow(`"message":"receipt array failure"`); + } finally { + globalThis.fetch = originalFetch; + } + }); + it('rejects empty successful IAPKit responses as receipt errors', async () => { const service = createService(); const originalFetch = globalThis.fetch; diff --git a/libraries/react-native-iap/src/vega-adapter.ts b/libraries/react-native-iap/src/vega-adapter.ts index da80184c..875a89b6 100644 --- a/libraries/react-native-iap/src/vega-adapter.ts +++ b/libraries/react-native-iap/src/vega-adapter.ts @@ -611,7 +611,10 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { const errors = record.errors; if (Array.isArray(errors) && errors.length > 0) { - return extractIapkitErrorMessage(errors[0], depth + 1); + const firstError = errors[0]; + return typeof firstError === 'string' + ? extractStringMessage(firstError) + : extractIapkitErrorMessage(firstError, depth + 1); } if (typeof record.message === 'string') { diff --git a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt index 8f616ca8..53e123f0 100644 --- a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt @@ -605,8 +605,9 @@ class OpenIapModule( var pageCount = 0 do { if (pageCount >= AMAZON_PURCHASE_UPDATES_MAX_PAGES) { - OpenIapLog.w("Amazon purchase updates exceeded pagination limit", TAG) - break + throw OpenIapError.ServiceTimeout( + "Amazon purchase updates exceeded pagination limit ($AMAZON_PURCHASE_UPDATES_MAX_PAGES pages)" + ) } pageCount += 1 val response = awaitPurchaseUpdates(shouldReset) From f3bce1cae3c6c7f191196fcfe5389f733e335878 Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 25 May 2026 08:53:36 +0900 Subject: [PATCH 10/51] fix(amazon): parse localized prices first --- .../amazon/java/dev/hyo/openiap/OpenIapModule.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt index 53e123f0..97e4892c 100644 --- a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt @@ -32,6 +32,8 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import java.lang.ref.WeakReference +import java.text.NumberFormat +import java.text.ParsePosition import java.util.Locale import java.util.concurrent.ConcurrentHashMap import com.amazon.device.iap.model.Product as AmazonProduct @@ -800,6 +802,8 @@ class OpenIapModule( val value = this?.trim().orEmpty() if (value.isEmpty()) return 0.0 + parseLocalizedPrice(value)?.let { return it } + val numeric = value.replace(Regex("[^0-9,.-]"), "") if (numeric.isBlank()) return 0.0 @@ -819,6 +823,18 @@ class OpenIapModule( return normalized.toDoubleOrNull() ?: 0.0 } + private fun parseLocalizedPrice(value: String): Double? { + val locale = Locale.getDefault() + return listOf( + NumberFormat.getCurrencyInstance(locale), + NumberFormat.getNumberInstance(locale) + ).firstNotNullOfOrNull { format -> + val position = ParsePosition(0) + val parsed = format.parse(value, position) + if (parsed != null && position.index > 0) parsed.toDouble() else null + } + } + private fun AmazonReceipt.toPurchase(): PurchaseAndroid { val isSubscription = productType == AmazonProductType.SUBSCRIPTION val dateMillis = purchaseDate?.time?.toDouble() ?: 0.0 From 08e86fe681158475b44e8d66d68d7ef70fcc06b3 Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 25 May 2026 09:06:55 +0900 Subject: [PATCH 11/51] fix(runtime): address review followups --- .../main/java/expo/modules/iap/ExpoIapLog.kt | 11 +++++- libraries/expo-iap/plugin/src/withIAP.ts | 2 +- libraries/expo-iap/src/ExpoIapModule.ts | 36 +++++++++---------- .../java/dev/hyo/openiap/OpenIapModule.kt | 12 ++++++- packages/kit/convex/purchases/amazon.ts | 2 +- 5 files changed, 41 insertions(+), 22 deletions(-) diff --git a/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapLog.kt b/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapLog.kt index 9180039a..722eeb30 100644 --- a/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapLog.kt +++ b/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapLog.kt @@ -75,7 +75,16 @@ internal object ExpoIapLog { private fun isSensitiveKey(key: String): Boolean { val normalized = key.lowercase().filter { it.isLetterOrDigit() } - return listOf("token", "apikey", "secret", "jws", "receiptid", "userid").any { + return listOf( + "token", + "apikey", + "secret", + "jws", + "receiptid", + "userid", + "password", + "auth" + ).any { normalized.contains(it) } } diff --git a/libraries/expo-iap/plugin/src/withIAP.ts b/libraries/expo-iap/plugin/src/withIAP.ts index e0b23ac7..2585f381 100644 --- a/libraries/expo-iap/plugin/src/withIAP.ts +++ b/libraries/expo-iap/plugin/src/withIAP.ts @@ -116,7 +116,7 @@ export function syncHorizonAppIdMetaData( !manifest.manifest.application || manifest.manifest.application.length === 0 ) { - manifest.manifest.application = [{$: {'android:name': '.MainApplication'}}]; + manifest.manifest.application = [{$: {}}]; } const horizonApplication = manifest.manifest.application[0]!; diff --git a/libraries/expo-iap/src/ExpoIapModule.ts b/libraries/expo-iap/src/ExpoIapModule.ts index 7b97ad71..a25c2c17 100644 --- a/libraries/expo-iap/src/ExpoIapModule.ts +++ b/libraries/expo-iap/src/ExpoIapModule.ts @@ -84,6 +84,24 @@ function isMissingModuleError(error: unknown, moduleName: string): boolean { return false; } +function getExpoIapFallbackModule(): any | null { + if (expoIapFallback !== undefined) { + return expoIapFallback; + } + + try { + expoIapFallback = requireNativeModule('ExpoIap'); + } catch (error) { + if (isMissingModuleError(error, 'ExpoIap')) { + expoIapFallback = null; + } else { + throw error; + } + } + + return expoIapFallback; +} + export const NATIVE_ERROR_CODES: Record = new Proxy( {} as Record, { @@ -106,24 +124,6 @@ export function getNativeModule() { export default new Proxy({} as any, { get(target, prop) { - function getExpoIapFallbackModule(): any | null { - if (expoIapFallback !== undefined) { - return expoIapFallback; - } - - try { - expoIapFallback = requireNativeModule('ExpoIap'); - } catch (error) { - if (isMissingModuleError(error, 'ExpoIap')) { - expoIapFallback = null; - } else { - throw error; - } - } - - return expoIapFallback; - } - if (typeof prop === 'symbol') return Reflect.get(target, prop); const resolved = getResolved(); if (prop === 'USING_ONSIDE_SDK') { diff --git a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt index 97e4892c..04724b34 100644 --- a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt @@ -34,6 +34,7 @@ import kotlinx.coroutines.withTimeout import java.lang.ref.WeakReference import java.text.NumberFormat import java.text.ParsePosition +import java.util.Currency import java.util.Locale import java.util.concurrent.ConcurrentHashMap import com.amazon.device.iap.model.Product as AmazonProduct @@ -812,7 +813,9 @@ class OpenIapModule( val decimalIndex = maxOf(lastDot, lastComma) val hasMixedSeparators = lastDot >= 0 && lastComma >= 0 val fractionLength = if (decimalIndex >= 0) numeric.length - decimalIndex - 1 else 0 - val hasDecimalSeparator = decimalIndex >= 0 && (hasMixedSeparators || fractionLength in 1..2) + val currencyFractionDigits = value.currencyFractionDigits() + val hasDecimalSeparator = decimalIndex >= 0 && + (hasMixedSeparators || fractionLength in 1..2 || fractionLength == currencyFractionDigits) val normalized = if (hasDecimalSeparator) { val integerPart = numeric.substring(0, decimalIndex).replace(Regex("[^0-9-]"), "") val fractionPart = numeric.substring(decimalIndex + 1).replace(Regex("[^0-9]"), "") @@ -835,6 +838,13 @@ class OpenIapModule( } } + private fun String.currencyFractionDigits(): Int? { + val code = Regex("\\b[A-Z]{3}\\b").find(this)?.value ?: return null + return runCatching { Currency.getInstance(code).defaultFractionDigits } + .getOrNull() + ?.takeIf { it >= 0 } + } + private fun AmazonReceipt.toPurchase(): PurchaseAndroid { val isSubscription = productType == AmazonProductType.SUBSCRIPTION val dateMillis = purchaseDate?.time?.toDouble() ?: 0.0 diff --git a/packages/kit/convex/purchases/amazon.ts b/packages/kit/convex/purchases/amazon.ts index 43c2b3de..62ed57e6 100644 --- a/packages/kit/convex/purchases/amazon.ts +++ b/packages/kit/convex/purchases/amazon.ts @@ -169,7 +169,7 @@ export const verifyAmazonReceiptInternalV1 = action({ receiptId: args.receiptId, sandbox, }; - const applicationId = project.androidPackageName ?? "amazon-appstore"; + const applicationId = project.androidPackageName ?? `amazon:${project._id}`; const remoteId = buildAmazonRemoteId({ userId: args.userId, receiptId: args.receiptId, From 331865de5ba41b609c9c9278aced94d1e5dc7514 Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 25 May 2026 09:28:52 +0900 Subject: [PATCH 12/51] fix(runtime): complete vega rn adapter --- .../src/__tests__/utils/error.test.ts | 23 ++ .../src/__tests__/vega-adapter.test.ts | 104 +++++++-- libraries/react-native-iap/src/utils/error.ts | 31 +++ .../react-native-iap/src/vega-adapter.ts | 201 ++++++++++++++++-- 4 files changed, 327 insertions(+), 32 deletions(-) diff --git a/libraries/react-native-iap/src/__tests__/utils/error.test.ts b/libraries/react-native-iap/src/__tests__/utils/error.test.ts index 30b72cc9..44c93733 100644 --- a/libraries/react-native-iap/src/__tests__/utils/error.test.ts +++ b/libraries/react-native-iap/src/__tests__/utils/error.test.ts @@ -47,6 +47,29 @@ describe('Error utilities', () => { }); }); + it('should preserve structured fields on Error objects', () => { + const error = new Error('Network error') as Error & { + code?: ErrorCode; + debugMessage?: string; + productId?: string; + responseCode?: number; + }; + error.code = ErrorCode.NetworkError; + error.debugMessage = 'Network error'; + error.productId = 'premium_monthly'; + error.responseCode = 503; + + const result = parseErrorStringToJsonObj(error); + + expect(result).toEqual({ + code: ErrorCode.NetworkError, + message: 'Network error', + debugMessage: 'Network error', + productId: 'premium_monthly', + responseCode: 503, + }); + }); + it('should handle non-JSON string', () => { const errorString = 'Not a JSON string'; diff --git a/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts b/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts index 998a0b3e..f8c935cc 100644 --- a/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts +++ b/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts @@ -182,7 +182,10 @@ describe('Amazon Vega adapter', () => { module.requestPurchase({ android: {skus: ['missing_sku']}, }), - ).rejects.toThrow(`"code":"${ErrorCode.SkuNotFound}"`); + ).rejects.toMatchObject({ + code: ErrorCode.SkuNotFound, + responseCode: 2, + }); expect(errorListener).toHaveBeenCalledWith( expect.objectContaining({ code: ErrorCode.SkuNotFound, @@ -288,6 +291,71 @@ describe('Amazon Vega adapter', () => { ]); }); + it('uses serialized subscription request context for direct Nitro calls', async () => { + const service = createService(); + service.purchase.mockResolvedValueOnce({ + responseCode: 0, + receipt: { + receiptId: 'sub-purchase', + sku: 'premium_monthly', + }, + }); + const module = createVegaIapModule(service); + + await expect( + module.requestPurchase({ + android: { + skus: ['premium_monthly'], + subscriptionOffers: '[]' as any, + }, + }), + ).resolves.toEqual([ + expect.objectContaining({ + productId: 'premium_monthly', + isAutoRenewing: true, + autoRenewingAndroid: true, + }), + ]); + }); + + it('exposes direct finish helpers and unsupported stubs on Vega', async () => { + const service = createService(); + const module = createVegaIapModule(service) as ReturnType< + typeof createVegaIapModule + > & { + acknowledgePurchaseAndroid(purchaseToken: string): Promise; + addSubscriptionBillingIssueListener(listener?: unknown): void; + consumePurchaseAndroid(purchaseToken: string): Promise; + deepLinkToSubscriptionsAndroid(options: unknown): Promise; + restorePurchases(): Promise; + }; + + await expect(module.acknowledgePurchaseAndroid('receipt-1')).resolves.toBe( + true, + ); + await expect(module.consumePurchaseAndroid('receipt-2')).resolves.toBe( + true, + ); + await expect(module.restorePurchases()).resolves.toBeUndefined(); + expect(module.addSubscriptionBillingIssueListener).not.toThrow(); + await expect( + module.deepLinkToSubscriptionsAndroid({ + packageNameAndroid: 'dev.hyo.openiap', + skuAndroid: 'premium_monthly', + }), + ).rejects.toMatchObject({ + code: ErrorCode.FeatureNotSupported, + }); + expect(service.notifyFulfillment).toHaveBeenCalledWith({ + fulfillmentResult: 1, + receiptId: 'receipt-1', + }); + expect(service.notifyFulfillment).toHaveBeenCalledWith({ + fulfillmentResult: 1, + receiptId: 'receipt-2', + }); + }); + it('loads all paginated Amazon purchase updates', async () => { const service = createService(); service.getPurchaseUpdates @@ -440,7 +508,7 @@ describe('Amazon Vega adapter', () => { }, }), ).rejects.toThrow( - `"message":"Amazon Vega IAPKit verification requires exactly one amazon payload."`, + 'Amazon Vega IAPKit verification requires exactly one amazon payload.', ); }); @@ -466,7 +534,7 @@ describe('Amazon Vega adapter', () => { }, }, }), - ).rejects.toThrow(`"message":"HTTP 502"`); + ).rejects.toThrow('HTTP 502'); } finally { globalThis.fetch = originalFetch; } @@ -501,7 +569,7 @@ describe('Amazon Vega adapter', () => { }, }, }), - ).rejects.toThrow(`"message":"receipt no longer valid"`); + ).rejects.toThrow('receipt no longer valid'); } finally { globalThis.fetch = originalFetch; } @@ -534,7 +602,7 @@ describe('Amazon Vega adapter', () => { }, }, }), - ).rejects.toThrow(`"message":"receipt array failure"`); + ).rejects.toThrow('receipt array failure'); } finally { globalThis.fetch = originalFetch; } @@ -562,9 +630,7 @@ describe('Amazon Vega adapter', () => { }, }, }), - ).rejects.toThrow( - `"message":"IAPKit returned non-JSON response (HTTP 200)."`, - ); + ).rejects.toThrow('IAPKit returned non-JSON response (HTTP 200).'); } finally { globalThis.fetch = originalFetch; } @@ -602,7 +668,7 @@ describe('Amazon Vega adapter', () => { }, }, }), - ).rejects.toThrow(`"message":"bad receipt"`); + ).rejects.toThrow('bad receipt'); } finally { globalThis.fetch = originalFetch; } @@ -630,9 +696,7 @@ describe('Amazon Vega adapter', () => { }, }, }), - ).rejects.toThrow( - `"message":"IAPKit returned malformed response (HTTP 200)."`, - ); + ).rejects.toThrow('IAPKit returned malformed response (HTTP 200).'); } finally { globalThis.fetch = originalFetch; } @@ -666,9 +730,7 @@ describe('Amazon Vega adapter', () => { }, }, }), - ).rejects.toThrow( - `"message":"IAPKit returned malformed response (HTTP 200)."`, - ); + ).rejects.toThrow('IAPKit returned malformed response (HTTP 200).'); } finally { globalThis.fetch = originalFetch; } @@ -703,9 +765,7 @@ describe('Amazon Vega adapter', () => { }, }, }), - ).rejects.toThrow( - `"message":"IAPKit returned malformed response (HTTP 200)."`, - ); + ).rejects.toThrow('IAPKit returned malformed response (HTTP 200).'); } finally { globalThis.fetch = originalFetch; } @@ -732,7 +792,9 @@ describe('Amazon Vega adapter', () => { }, }, }), - ).rejects.toThrow(`"code":"network-error"`); + ).rejects.toMatchObject({ + code: ErrorCode.NetworkError, + }); } finally { globalThis.fetch = originalFetch; } @@ -763,7 +825,9 @@ describe('Amazon Vega adapter', () => { }, }, }), - ).rejects.toThrow(`"code":"network-error"`); + ).rejects.toMatchObject({ + code: ErrorCode.NetworkError, + }); } finally { globalThis.fetch = originalFetch; } diff --git a/libraries/react-native-iap/src/utils/error.ts b/libraries/react-native-iap/src/utils/error.ts index c5abd361..0d0872c6 100644 --- a/libraries/react-native-iap/src/utils/error.ts +++ b/libraries/react-native-iap/src/utils/error.ts @@ -32,6 +32,37 @@ export function parseErrorStringToJsonObj( ): IapError { // Handle Error objects if (errorString instanceof Error) { + const errorWithFields = errorString as Error & Partial; + if ( + errorWithFields.code != null || + errorWithFields.responseCode != null || + errorWithFields.debugMessage != null || + errorWithFields.productId != null + ) { + const parsed: IapError = { + code: String(errorWithFields.code ?? ErrorCode.Unknown), + message: errorString.message, + }; + if (errorWithFields.responseCode !== undefined) { + parsed.responseCode = errorWithFields.responseCode; + } + if (errorWithFields.debugMessage !== undefined) { + parsed.debugMessage = errorWithFields.debugMessage; + } + if (errorWithFields.productId !== undefined) { + parsed.productId = errorWithFields.productId; + } + if (errorWithFields.productIds !== undefined) { + parsed.productIds = errorWithFields.productIds; + } + if (errorWithFields.productType !== undefined) { + parsed.productType = errorWithFields.productType; + } + if (errorWithFields.isEmptyProductList !== undefined) { + parsed.isEmptyProductList = errorWithFields.isEmptyProductList; + } + return parsed; + } errorString = errorString.message; } diff --git a/libraries/react-native-iap/src/vega-adapter.ts b/libraries/react-native-iap/src/vega-adapter.ts index 875a89b6..4bdcd433 100644 --- a/libraries/react-native-iap/src/vega-adapter.ts +++ b/libraries/react-native-iap/src/vega-adapter.ts @@ -79,6 +79,14 @@ interface VegaUserDataResponse extends VegaResponse { userData?: VegaUserData | null; } +interface VegaError extends Error { + code?: ErrorCode; + debugMessage?: string; + platform?: 'android'; + productId?: string; + responseCode?: number; +} + export interface VegaPurchasingService { getProductData(request: {skus: string[]}): Promise; getPurchaseUpdates(request: { @@ -106,20 +114,35 @@ function createVegaError( responseCode?: unknown, productId?: string, ): Error { - return new Error( - JSON.stringify({ - code, - message, - responseCode: typeof responseCode === 'number' ? responseCode : undefined, - debugMessage: message, - productId, - platform: 'android', - }), - ); + const error = new Error(message) as VegaError; + error.code = code; + error.debugMessage = message; + error.platform = 'android'; + error.productId = productId; + if (typeof responseCode === 'number') { + error.responseCode = responseCode; + } + return error; } function parseVegaErrorPayload(error: unknown): Record { if (!(error instanceof Error)) return {}; + const vegaError = error as VegaError; + if ( + vegaError.code != null || + vegaError.debugMessage != null || + vegaError.productId != null || + vegaError.responseCode != null + ) { + return { + code: vegaError.code, + message: error.message, + responseCode: vegaError.responseCode, + debugMessage: vegaError.debugMessage, + productId: vegaError.productId, + platform: vegaError.platform, + }; + } try { const parsed = JSON.parse(error.message); return parsed && typeof parsed === 'object' @@ -412,6 +435,27 @@ function getSkuFromRequest(request: Parameters[0]) { return skus[0]!; } +function hasSubscriptionRequestContext(subscriptionOffers: unknown): boolean { + if (Array.isArray(subscriptionOffers)) return true; + if (typeof subscriptionOffers === 'string') { + return subscriptionOffers.trim().length > 0; + } + return subscriptionOffers != null; +} + +function throwUnsupportedFeature(feature: string): never { + throw createVegaError( + ErrorCode.FeatureNotSupported, + `${feature} is not supported on Amazon Vega.`, + ); +} + +type VegaRnIapModule = Partial & { + acknowledgePurchaseAndroid(purchaseToken: string): Promise; + consumePurchaseAndroid(purchaseToken: string): Promise; + restorePurchases(): Promise; +}; + export function createVegaIapModule(service: VegaPurchasingService): RnIap { const productTypesBySku = new Map(); const purchaseUpdateListeners = new Map< @@ -801,7 +845,7 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { }; }; - const module: Partial = { + const module: VegaRnIapModule = { async initConnection(): Promise { await getStorefront(); return true; @@ -842,7 +886,7 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { try { sku = getSkuFromRequest(request); const androidRequest = request.google ?? request.android; - const fallbackProductType = Array.isArray( + const fallbackProductType = hasSubscriptionRequestContext( androidRequest?.subscriptionOffers, ) ? PRODUCT_TYPE_SUBSCRIPTION @@ -907,6 +951,19 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { const token = params.android?.purchaseToken; return finishReceipt(token ?? ''); }, + async acknowledgePurchaseAndroid(purchaseToken): Promise { + await finishReceipt(purchaseToken); + return true; + }, + async consumePurchaseAndroid(purchaseToken): Promise { + await finishReceipt(purchaseToken); + return true; + }, + async restorePurchases(): Promise { + await getAvailablePurchases({ + android: {includeSuspended: false}, + }); + }, addPurchaseUpdatedListener(listener): number { const token = nextPurchaseUpdateListenerToken++; purchaseUpdateListeners.set(token, listener); @@ -923,6 +980,72 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { }, addPromotedProductListenerIOS(): void {}, removePromotedProductListenerIOS(): void {}, + async getAppTransactionIOS(): Promise { + return throwUnsupportedFeature('getAppTransactionIOS'); + }, + async requestPromotedProductIOS(): Promise { + return throwUnsupportedFeature('requestPromotedProductIOS'); + }, + async getPromotedProductIOS(): Promise { + return throwUnsupportedFeature('getPromotedProductIOS'); + }, + async buyPromotedProductIOS(): Promise { + return throwUnsupportedFeature('buyPromotedProductIOS'); + }, + async presentCodeRedemptionSheetIOS(): Promise { + return throwUnsupportedFeature('presentCodeRedemptionSheetIOS'); + }, + async clearTransactionIOS(): Promise { + return throwUnsupportedFeature('clearTransactionIOS'); + }, + async beginRefundRequestIOS(): Promise { + return throwUnsupportedFeature('beginRefundRequestIOS'); + }, + async subscriptionStatusIOS(): Promise { + return throwUnsupportedFeature('subscriptionStatusIOS'); + }, + async currentEntitlementIOS(): Promise { + return throwUnsupportedFeature('currentEntitlementIOS'); + }, + async latestTransactionIOS(): Promise { + return throwUnsupportedFeature('latestTransactionIOS'); + }, + async getPendingTransactionsIOS(): Promise { + return throwUnsupportedFeature('getPendingTransactionsIOS'); + }, + async getAllTransactionsIOS(): Promise { + return throwUnsupportedFeature('getAllTransactionsIOS'); + }, + async syncIOS(): Promise { + return throwUnsupportedFeature('syncIOS'); + }, + async showManageSubscriptionsIOS(): Promise { + return throwUnsupportedFeature('showManageSubscriptionsIOS'); + }, + async deepLinkToSubscriptionsIOS(): Promise { + return throwUnsupportedFeature('deepLinkToSubscriptionsIOS'); + }, + async isEligibleForIntroOfferIOS(): Promise { + return throwUnsupportedFeature('isEligibleForIntroOfferIOS'); + }, + async getReceiptDataIOS(): Promise { + return throwUnsupportedFeature('getReceiptDataIOS'); + }, + async getReceiptIOS(): Promise { + return throwUnsupportedFeature('getReceiptIOS'); + }, + async requestReceiptRefreshIOS(): Promise { + return throwUnsupportedFeature('requestReceiptRefreshIOS'); + }, + async isTransactionVerifiedIOS(): Promise { + return throwUnsupportedFeature('isTransactionVerifiedIOS'); + }, + async getTransactionJwsIOS(): Promise { + return throwUnsupportedFeature('getTransactionJwsIOS'); + }, + async validateReceipt(): Promise { + return throwUnsupportedFeature('validateReceipt'); + }, async getStorefront(): Promise { return getStorefront(); }, @@ -932,6 +1055,60 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { async verifyPurchaseWithProvider(params) { return verifyWithIapkit(params); }, + async deepLinkToSubscriptionsAndroid(): Promise { + return throwUnsupportedFeature('deepLinkToSubscriptionsAndroid'); + }, + async checkAlternativeBillingAvailabilityAndroid(): Promise { + return throwUnsupportedFeature( + 'checkAlternativeBillingAvailabilityAndroid', + ); + }, + async showAlternativeBillingDialogAndroid(): Promise { + return throwUnsupportedFeature('showAlternativeBillingDialogAndroid'); + }, + async createAlternativeBillingTokenAndroid(): Promise { + return throwUnsupportedFeature('createAlternativeBillingTokenAndroid'); + }, + addUserChoiceBillingListenerAndroid(): void {}, + removeUserChoiceBillingListenerAndroid(): void {}, + addDeveloperProvidedBillingListenerAndroid(): void {}, + removeDeveloperProvidedBillingListenerAndroid(): void {}, + addSubscriptionBillingIssueListener(): void {}, + removeSubscriptionBillingIssueListener(): void {}, + enableBillingProgramAndroid(): void { + throwUnsupportedFeature('enableBillingProgramAndroid'); + }, + async isBillingProgramAvailableAndroid(): Promise { + return throwUnsupportedFeature('isBillingProgramAvailableAndroid'); + }, + async createBillingProgramReportingDetailsAndroid(): Promise { + return throwUnsupportedFeature( + 'createBillingProgramReportingDetailsAndroid', + ); + }, + async launchExternalLinkAndroid(): Promise { + return throwUnsupportedFeature('launchExternalLinkAndroid'); + }, + async canPresentExternalPurchaseNoticeIOS(): Promise { + return throwUnsupportedFeature('canPresentExternalPurchaseNoticeIOS'); + }, + async presentExternalPurchaseNoticeSheetIOS(): Promise { + return throwUnsupportedFeature('presentExternalPurchaseNoticeSheetIOS'); + }, + async presentExternalPurchaseLinkIOS(): Promise { + return throwUnsupportedFeature('presentExternalPurchaseLinkIOS'); + }, + async isEligibleForExternalPurchaseCustomLinkIOS(): Promise { + return throwUnsupportedFeature( + 'isEligibleForExternalPurchaseCustomLinkIOS', + ); + }, + async getExternalPurchaseCustomLinkTokenIOS(): Promise { + return throwUnsupportedFeature('getExternalPurchaseCustomLinkTokenIOS'); + }, + async showExternalPurchaseCustomLinkNoticeIOS(): Promise { + return throwUnsupportedFeature('showExternalPurchaseCustomLinkNoticeIOS'); + }, }; return module as RnIap; From 5df37428c6faa295971d81f37bc4f6581ac08eec Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 25 May 2026 09:41:28 +0900 Subject: [PATCH 13/51] fix(amazon): emit vega restore updates --- .../src/__tests__/vega-adapter.test.ts | 8 ++++++++ libraries/react-native-iap/src/vega-adapter.ts | 3 ++- .../java/dev/hyo/openiap/OpenIapModule.kt | 17 ++++++++++++----- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts b/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts index f8c935cc..bd559581 100644 --- a/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts +++ b/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts @@ -336,7 +336,15 @@ describe('Amazon Vega adapter', () => { await expect(module.consumePurchaseAndroid('receipt-2')).resolves.toBe( true, ); + const listener = jest.fn(); + module.addPurchaseUpdatedListener(listener); await expect(module.restorePurchases()).resolves.toBeUndefined(); + expect(listener).toHaveBeenCalledWith( + expect.objectContaining({ + productId: 'premium_monthly', + purchaseToken: 'sub-receipt', + }), + ); expect(module.addSubscriptionBillingIssueListener).not.toThrow(); await expect( module.deepLinkToSubscriptionsAndroid({ diff --git a/libraries/react-native-iap/src/vega-adapter.ts b/libraries/react-native-iap/src/vega-adapter.ts index 4bdcd433..3f96a86e 100644 --- a/libraries/react-native-iap/src/vega-adapter.ts +++ b/libraries/react-native-iap/src/vega-adapter.ts @@ -960,9 +960,10 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { return true; }, async restorePurchases(): Promise { - await getAvailablePurchases({ + const purchases = await getAvailablePurchases({ android: {includeSuspended: false}, }); + purchases.forEach(emitPurchaseUpdated); }, addPurchaseUpdatedListener(listener): number { const token = nextPurchaseUpdateListenerToken++; diff --git a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt index 04724b34..4042940b 100644 --- a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt @@ -848,16 +848,23 @@ class OpenIapModule( private fun AmazonReceipt.toPurchase(): PurchaseAndroid { val isSubscription = productType == AmazonProductType.SUBSCRIPTION val dateMillis = purchaseDate?.time?.toDouble() ?: 0.0 - val isCanceled = cancelDate != null - val state = if (isCanceled) PurchaseState.Unknown else PurchaseState.Purchased + val receiptCanceled = isCanceled || cancelDate != null + val receiptDeferred = isDeferred + val state = when { + receiptDeferred -> PurchaseState.Pending + receiptCanceled -> PurchaseState.Unknown + else -> PurchaseState.Purchased + } return PurchaseAndroid( - autoRenewingAndroid = isSubscription && !isCanceled, + autoRenewingAndroid = + isSubscription && !receiptCanceled && !receiptDeferred, currentPlanId = if (isSubscription) sku else null, dataAndroid = toJSON().toString(), id = receiptId, ids = listOf(sku), isAcknowledgedAndroid = null, - isAutoRenewing = isSubscription && !isCanceled, + isAutoRenewing = + isSubscription && !receiptCanceled && !receiptDeferred, packageNameAndroid = context.packageName, platform = IapPlatform.Android, productId = sku, @@ -868,7 +875,7 @@ class OpenIapModule( store = IapStore.Amazon, transactionDate = dateMillis, transactionId = receiptId, - isSuspendedAndroid = false + isSuspendedAndroid = receiptDeferred ) } From 602d4fc57bff9c5e66d6a6fa8b4808e0046f5750 Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 25 May 2026 09:51:55 +0900 Subject: [PATCH 14/51] fix(amazon): guard fire os request ids --- .../java/dev/hyo/openiap/OpenIapModule.kt | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt index 4042940b..bf3ecc61 100644 --- a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt @@ -574,7 +574,12 @@ class OpenIapModule( private suspend fun requestUserData(): UserDataResponse { val requestId = withContext(Dispatchers.Main) { ensureRegistered() - PurchasingService.getUserData().toString() + val request = runCatching { PurchasingService.getUserData() } + .getOrElse { + throw OpenIapError.InitConnection + } + ?: throw OpenIapError.InitConnection + request.toString() } return awaitAmazonResponse(requestId, userDataRequests, earlyUserDataResponses) } @@ -582,7 +587,17 @@ class OpenIapModule( private suspend fun requestProductData(skus: List): ProductDataResponse { val requestId = withContext(Dispatchers.Main) { ensureRegistered() - PurchasingService.getProductData(skus.toSet()).toString() + val request = runCatching { + PurchasingService.getProductData(skus.toSet()) + } + .getOrElse { error -> + throw error.toOpenIapError("Amazon getProductData request failed") + } + ?: throw OpenIapError.QueryProduct.withDiagnostics( + debugMessage = "Amazon getProductData failed to return a requestId", + productIds = skus + ) + request.toString() } return awaitAmazonResponse(requestId, productDataRequests, earlyProductDataResponses) } @@ -644,9 +659,20 @@ class OpenIapModule( private suspend fun awaitPurchaseUpdates(reset: Boolean): PurchaseUpdatesResponse { val requestId = withContext(Dispatchers.Main) { ensureRegistered() - PurchasingService.getPurchaseUpdates(reset).toString() + val request = runCatching { + PurchasingService.getPurchaseUpdates(reset) + } + .getOrElse { + throw OpenIapError.RestoreFailed + } + ?: throw OpenIapError.RestoreFailed + request.toString() } - return awaitAmazonResponse(requestId, purchaseUpdatesRequests, earlyPurchaseUpdatesResponses) + return awaitAmazonResponse( + requestId, + purchaseUpdatesRequests, + earlyPurchaseUpdatesResponses + ) } private suspend fun awaitAmazonResponse( From a29b06516d6d265840f9f5f9a603299a27bc7c28 Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 25 May 2026 10:09:58 +0900 Subject: [PATCH 15/51] fix(amazon): address vega review followups --- .../src/__tests__/vega-adapter.test.ts | 41 ++++++- libraries/expo-iap/src/vega-adapter.ts | 34 ++++-- .../src/__tests__/vega-adapter.test.ts | 39 ++++++ .../react-native-iap/src/vega-adapter.ts | 23 +++- .../java/dev/hyo/openiap/OpenIapModule.kt | 113 +++++++++++------- .../dev/hyo/openiap/AmazonPriceParserTest.kt | 44 +++++++ 6 files changed, 239 insertions(+), 55 deletions(-) create mode 100644 packages/google/openiap/src/testAmazon/java/dev/hyo/openiap/AmazonPriceParserTest.kt diff --git a/libraries/expo-iap/src/__tests__/vega-adapter.test.ts b/libraries/expo-iap/src/__tests__/vega-adapter.test.ts index 6067a5e0..5b4a3e12 100644 --- a/libraries/expo-iap/src/__tests__/vega-adapter.test.ts +++ b/libraries/expo-iap/src/__tests__/vega-adapter.test.ts @@ -65,7 +65,7 @@ const createService = (): jest.Mocked => notifyFulfillment: jest.fn(async () => ({ responseCode: 1, })), - }) as unknown as jest.Mocked; + } as unknown as jest.Mocked); describe('Amazon Vega Expo adapter', () => { it('maps Vega product data to OpenIAP Android products', async () => { @@ -415,6 +415,45 @@ describe('Amazon Vega Expo adapter', () => { } }); + it('supports custom IAPKit base URLs for Vega verification', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + Response.json({ + isValid: true, + state: 'ENTITLED', + store: 'amazon', + }), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createExpoIapVegaModule(service); + + await module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + apiKey: 'kit-key', + baseUrl: 'http://localhost:3100/', + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + } as Parameters[0] & { + iapkit: {baseUrl: string}; + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'http://localhost:3100/v1/purchase/verify', + expect.any(Object), + ); + } finally { + globalThis.fetch = originalFetch; + } + }); + it('rejects mixed IAPKit payloads on the Amazon Vega adapter', async () => { const service = createService(); const module = createExpoIapVegaModule(service); diff --git a/libraries/expo-iap/src/vega-adapter.ts b/libraries/expo-iap/src/vega-adapter.ts index 02b491e3..d0fff375 100644 --- a/libraries/expo-iap/src/vega-adapter.ts +++ b/libraries/expo-iap/src/vega-adapter.ts @@ -131,7 +131,26 @@ const PRODUCT_TYPE_SUBSCRIPTION = 3; const FULFILLMENT_RESULT_FULFILLED = 1; const RESPONSE_SUCCESS = 1; const PURCHASE_RESPONSE_SUCCESS = 0; -const IAPKIT_VERIFY_URL = 'https://kit.openiap.dev/v1/purchase/verify'; +const IAPKIT_DEFAULT_BASE_URL = 'https://kit.openiap.dev'; +const IAPKIT_VERIFY_PATH = '/v1/purchase/verify'; + +type IapkitEndpointOptions = NonNullable< + VerifyPurchaseWithProviderProps['iapkit'] +> & { + baseUrl?: string | null; +}; + +function iapkitVerifyUrl( + iapkit: VerifyPurchaseWithProviderProps['iapkit'], +): string { + const endpointOptions = iapkit as IapkitEndpointOptions | null | undefined; + const baseUrl = + typeof endpointOptions?.baseUrl === 'string' && + endpointOptions.baseUrl.trim().length > 0 + ? endpointOptions.baseUrl.trim() + : IAPKIT_DEFAULT_BASE_URL; + return `${baseUrl.replace(/\/+$/, '')}${IAPKIT_VERIFY_PATH}`; +} function createVegaError( code: ErrorCode, @@ -568,7 +587,7 @@ export function createExpoIapVegaModule( try { const parsed = JSON.parse(value); return parsed && typeof parsed === 'object' - ? (extractIapkitErrorMessage(parsed, depth + 1) ?? value) + ? extractIapkitErrorMessage(parsed, depth + 1) ?? value : value; } catch { return value; @@ -702,7 +721,7 @@ export function createExpoIapVegaModule( () => controller.abort(), IAPKIT_VERIFY_TIMEOUT_MS, ); - response = await fetch(IAPKIT_VERIFY_URL, { + response = await fetch(iapkitVerifyUrl(iapkit), { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -885,11 +904,11 @@ export function createExpoIapVegaModule( renewalInfoIOS: null, autoRenewingAndroid: purchase.platform === 'android' - ? (( + ? ( purchase as Purchase & { autoRenewingAndroid?: boolean | null; } - ).autoRenewingAndroid ?? null) + ).autoRenewingAndroid ?? null : null, basePlanIdAndroid: null, currentPlanId: null, @@ -899,8 +918,9 @@ export function createExpoIapVegaModule( async hasActiveSubscriptions( subscriptionIds?: string[] | null, ): Promise { - const subscriptions = - await vegaModule.getActiveSubscriptions(subscriptionIds); + const subscriptions = await vegaModule.getActiveSubscriptions( + subscriptionIds, + ); return subscriptions.length > 0; }, async acknowledgePurchaseAndroid(purchaseToken): Promise { diff --git a/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts b/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts index bd559581..ddf2dcad 100644 --- a/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts +++ b/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts @@ -498,6 +498,45 @@ describe('Amazon Vega adapter', () => { } }); + it('supports custom IAPKit base URLs for Vega verification', async () => { + const service = createService(); + const originalFetch = globalThis.fetch; + const fetchMock = jest.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + Response.json({ + isValid: true, + state: 'ENTITLED', + store: 'amazon', + }), + ) as unknown as jest.MockedFunction; + globalThis.fetch = fetchMock; + + try { + const module = createVegaIapModule(service); + + await module.verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + apiKey: 'kit-key', + baseUrl: 'http://localhost:3100/', + amazon: { + userId: 'amazon-user', + receiptId: 'receipt-vega-1', + }, + }, + } as Parameters[0] & { + iapkit: {baseUrl: string}; + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'http://localhost:3100/v1/purchase/verify', + expect.any(Object), + ); + } finally { + globalThis.fetch = originalFetch; + } + }); + it('rejects mixed IAPKit payloads on the Amazon Vega adapter', async () => { const service = createService(); const module = createVegaIapModule(service); diff --git a/libraries/react-native-iap/src/vega-adapter.ts b/libraries/react-native-iap/src/vega-adapter.ts index 3f96a86e..8643db04 100644 --- a/libraries/react-native-iap/src/vega-adapter.ts +++ b/libraries/react-native-iap/src/vega-adapter.ts @@ -106,7 +106,26 @@ const RESPONSE_SUCCESS = 1; const PURCHASE_RESPONSE_SUCCESS = 0; const PURCHASE_STATE_PURCHASED = 1; const PURCHASE_STATE_PENDING = 2; -const IAPKIT_VERIFY_URL = 'https://kit.openiap.dev/v1/purchase/verify'; +const IAPKIT_DEFAULT_BASE_URL = 'https://kit.openiap.dev'; +const IAPKIT_VERIFY_PATH = '/v1/purchase/verify'; + +type IapkitEndpointOptions = NonNullable< + NitroVerifyPurchaseWithProviderProps['iapkit'] +> & { + baseUrl?: string | null; +}; + +function iapkitVerifyUrl( + iapkit: NitroVerifyPurchaseWithProviderProps['iapkit'], +): string { + const endpointOptions = iapkit as IapkitEndpointOptions | null | undefined; + const baseUrl = + typeof endpointOptions?.baseUrl === 'string' && + endpointOptions.baseUrl.trim().length > 0 + ? endpointOptions.baseUrl.trim() + : IAPKIT_DEFAULT_BASE_URL; + return `${baseUrl.replace(/\/+$/, '')}${IAPKIT_VERIFY_PATH}`; +} function createVegaError( code: ErrorCode, @@ -771,7 +790,7 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { () => controller.abort(), IAPKIT_VERIFY_TIMEOUT_MS, ); - response = await fetch(IAPKIT_VERIFY_URL, { + response = await fetch(iapkitVerifyUrl(iapkit), { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt index bf3ecc61..a628d016 100644 --- a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt @@ -46,6 +46,72 @@ private const val AMAZON_REQUEST_TIMEOUT_MS = 60_000L private const val AMAZON_PRODUCT_DATA_BATCH_SIZE = 100 private const val AMAZON_PURCHASE_UPDATES_MAX_PAGES = 100 +internal object AmazonPriceParser { + fun toPriceAmount(displayPrice: String?): Double { + val value = displayPrice?.trim().orEmpty() + if (value.isEmpty()) return 0.0 + + parseLocalizedPrice(value)?.let { return it } + + val numeric = value.replace(Regex("[^0-9,.-]"), "") + if (numeric.isBlank()) return 0.0 + + val lastDot = numeric.lastIndexOf('.') + val lastComma = numeric.lastIndexOf(',') + val decimalIndex = maxOf(lastDot, lastComma) + val hasMixedSeparators = lastDot >= 0 && lastComma >= 0 + val fractionLength = if (decimalIndex >= 0) { + numeric.length - decimalIndex - 1 + } else { + 0 + } + val currencyFractionDigits = value.currencyFractionDigits() + val separatorMatchesCurrency = currencyFractionDigits != null && + fractionLength == currencyFractionDigits + val hasDecimalSeparator = decimalIndex >= 0 && + (hasMixedSeparators || fractionLength in 1..2 || separatorMatchesCurrency) + val normalized = if (hasDecimalSeparator) { + val integerPart = numeric.substring(0, decimalIndex).replace(Regex("[^0-9-]"), "") + val fractionPart = numeric.substring(decimalIndex + 1).replace(Regex("[^0-9]"), "") + "$integerPart.$fractionPart" + } else { + numeric.replace(Regex("[^0-9-]"), "") + } + return normalized.toDoubleOrNull() ?: 0.0 + } + + private fun parseLocalizedPrice(value: String): Double? { + val locale = Locale.getDefault() + return listOf( + NumberFormat.getCurrencyInstance(locale), + NumberFormat.getNumberInstance(locale) + ).firstNotNullOfOrNull { format -> + val position = ParsePosition(0) + val parsed = format.parse(value, position) + if ( + parsed != null && + position.index > 0 && + !value.hasUnparsedPriceCharacters(position.index) + ) { + parsed.toDouble() + } else { + null + } + } + } + + private fun String.hasUnparsedPriceCharacters(startIndex: Int): Boolean { + return drop(startIndex).any { it.isDigit() || it == '.' || it == ',' || it == '-' } + } + + private fun String.currencyFractionDigits(): Int? { + val code = Regex("\\b[A-Z]{3}\\b").find(this)?.value ?: return null + return runCatching { Currency.getInstance(code).defaultFractionDigits } + .getOrNull() + ?.takeIf { it >= 0 } + } +} + /** * OpenIapModule for Amazon Appstore SDK IAP. * @@ -203,8 +269,7 @@ class OpenIapModule( override val getAvailablePurchases: QueryGetAvailablePurchasesHandler = { options -> withContext(Dispatchers.IO) { - ensureRegistered() - val purchases = requestPurchaseUpdates(reset = true) + val purchases = getAvailableItems(ProductQueryType.All) if (options?.includeSuspendedAndroid == true) { purchases } else { @@ -826,49 +891,7 @@ class OpenIapModule( } private fun String?.toPriceAmount(): Double { - val value = this?.trim().orEmpty() - if (value.isEmpty()) return 0.0 - - parseLocalizedPrice(value)?.let { return it } - - val numeric = value.replace(Regex("[^0-9,.-]"), "") - if (numeric.isBlank()) return 0.0 - - val lastDot = numeric.lastIndexOf('.') - val lastComma = numeric.lastIndexOf(',') - val decimalIndex = maxOf(lastDot, lastComma) - val hasMixedSeparators = lastDot >= 0 && lastComma >= 0 - val fractionLength = if (decimalIndex >= 0) numeric.length - decimalIndex - 1 else 0 - val currencyFractionDigits = value.currencyFractionDigits() - val hasDecimalSeparator = decimalIndex >= 0 && - (hasMixedSeparators || fractionLength in 1..2 || fractionLength == currencyFractionDigits) - val normalized = if (hasDecimalSeparator) { - val integerPart = numeric.substring(0, decimalIndex).replace(Regex("[^0-9-]"), "") - val fractionPart = numeric.substring(decimalIndex + 1).replace(Regex("[^0-9]"), "") - "$integerPart.$fractionPart" - } else { - numeric.replace(Regex("[^0-9-]"), "") - } - return normalized.toDoubleOrNull() ?: 0.0 - } - - private fun parseLocalizedPrice(value: String): Double? { - val locale = Locale.getDefault() - return listOf( - NumberFormat.getCurrencyInstance(locale), - NumberFormat.getNumberInstance(locale) - ).firstNotNullOfOrNull { format -> - val position = ParsePosition(0) - val parsed = format.parse(value, position) - if (parsed != null && position.index > 0) parsed.toDouble() else null - } - } - - private fun String.currencyFractionDigits(): Int? { - val code = Regex("\\b[A-Z]{3}\\b").find(this)?.value ?: return null - return runCatching { Currency.getInstance(code).defaultFractionDigits } - .getOrNull() - ?.takeIf { it >= 0 } + return AmazonPriceParser.toPriceAmount(this) } private fun AmazonReceipt.toPurchase(): PurchaseAndroid { diff --git a/packages/google/openiap/src/testAmazon/java/dev/hyo/openiap/AmazonPriceParserTest.kt b/packages/google/openiap/src/testAmazon/java/dev/hyo/openiap/AmazonPriceParserTest.kt new file mode 100644 index 00000000..8d1bc28f --- /dev/null +++ b/packages/google/openiap/src/testAmazon/java/dev/hyo/openiap/AmazonPriceParserTest.kt @@ -0,0 +1,44 @@ +package dev.hyo.openiap + +import java.util.Locale +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class AmazonPriceParserTest { + private lateinit var defaultLocale: Locale + + @Before + fun setUp() { + defaultLocale = Locale.getDefault() + Locale.setDefault(Locale.US) + } + + @After + fun tearDown() { + Locale.setDefault(defaultLocale) + } + + @Test + fun parsesInternationalFormattedPrices() { + val cases = mapOf( + "\$1,234.56" to 1234.56, + "\$1,234" to 1234.0, + "1.234,56 €" to 1234.56, + "1 234,56 руб" to 1234.56, + "JPY 1,000" to 1000.0, + "¥1000" to 1000.0, + "TND 1.234" to 1.234 + ) + + for ((displayPrice, expected) in cases) { + assertEquals( + displayPrice, + expected, + AmazonPriceParser.toPriceAmount(displayPrice), + 0.0001 + ) + } + } +} From 0c3093bc9922ee145d403bc9a3bb5abba45564ce Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 25 May 2026 10:24:04 +0900 Subject: [PATCH 16/51] fix(amazon): chunk vega product lookups --- .../src/__tests__/vega-adapter.test.ts | 91 +++++++++++++++++++ libraries/expo-iap/src/vega-adapter.ts | 50 +++++++--- .../src/__tests__/vega-adapter.test.ts | 76 ++++++++++++++++ .../react-native-iap/src/vega-adapter.ts | 40 +++++--- .../java/dev/hyo/openiap/OpenIapModule.kt | 83 ++++++++++++++--- 5 files changed, 304 insertions(+), 36 deletions(-) diff --git a/libraries/expo-iap/src/__tests__/vega-adapter.test.ts b/libraries/expo-iap/src/__tests__/vega-adapter.test.ts index 5b4a3e12..bb3ee71b 100644 --- a/libraries/expo-iap/src/__tests__/vega-adapter.test.ts +++ b/libraries/expo-iap/src/__tests__/vega-adapter.test.ts @@ -328,6 +328,97 @@ describe('Amazon Vega Expo adapter', () => { ]); }); + it('limits paginated Amazon purchase updates', async () => { + const service = createService(); + service.getPurchaseUpdates.mockResolvedValue({ + responseCode: 1, + hasMore: true, + receiptList: [], + }); + const module = createExpoIapVegaModule(service); + + await expect(module.getAvailableItems()).rejects.toMatchObject({ + code: ErrorCode.ServiceError, + }); + expect(service.getPurchaseUpdates).toHaveBeenCalledTimes(100); + }); + + it('chunks Vega product data requests', async () => { + const service = createService(); + const skus = Array.from({length: 101}, (_, index) => `sku_${index}`); + service.getProductData.mockImplementation(async ({skus: batch}) => ({ + responseCode: 1, + productData: Object.fromEntries( + batch.map((sku) => [ + sku, + { + sku, + title: sku, + description: 'Product', + productType: 1, + price: { + priceCurrencyCode: 'USD', + priceStr: '$0.99', + valueInMicros: 990000, + }, + }, + ]), + ), + })); + const module = createExpoIapVegaModule(service); + + const products = await module.fetchProducts('all', skus); + + expect(products).toHaveLength(101); + expect(service.getProductData).toHaveBeenCalledTimes(2); + expect(service.getProductData.mock.calls[0]?.[0].skus).toHaveLength(100); + expect(service.getProductData.mock.calls[1]?.[0].skus).toHaveLength(1); + }); + + it('chunks product type hydration for purchase updates', async () => { + const service = createService(); + const skus = Array.from({length: 101}, (_, index) => `sub_${index}`); + service.getPurchaseUpdates.mockResolvedValue({ + responseCode: 1, + receiptList: skus.map((sku) => ({ + receiptId: `receipt_${sku}`, + sku, + })), + }); + service.getProductData.mockImplementation(async ({skus: batch}) => ({ + responseCode: 1, + productData: Object.fromEntries( + batch.map((sku) => [ + sku, + { + sku, + title: sku, + description: 'Subscription', + productType: 3, + subscriptionPeriod: 'P1M', + price: { + priceCurrencyCode: 'USD', + priceStr: '$4.99', + valueInMicros: 4990000, + }, + }, + ]), + ), + })); + const module = createExpoIapVegaModule(service); + + const purchases = await module.getAvailableItems(); + + expect(purchases).toHaveLength(101); + expect(purchases[0]).toMatchObject({ + isAutoRenewing: true, + productId: 'sub_0', + }); + expect(service.getProductData).toHaveBeenCalledTimes(2); + expect(service.getProductData.mock.calls[0]?.[0].skus).toHaveLength(100); + expect(service.getProductData.mock.calls[1]?.[0].skus).toHaveLength(1); + }); + it('excludes suspended purchases unless requested', async () => { const service = createService(); service.getPurchaseUpdates.mockResolvedValue({ diff --git a/libraries/expo-iap/src/vega-adapter.ts b/libraries/expo-iap/src/vega-adapter.ts index d0fff375..6f0c9023 100644 --- a/libraries/expo-iap/src/vega-adapter.ts +++ b/libraries/expo-iap/src/vega-adapter.ts @@ -19,6 +19,8 @@ type ResponseOperation = const IAPKIT_VERIFY_TIMEOUT_MS = 10_000; const MAX_IAPKIT_ERROR_DEPTH = 5; +const MAX_PRODUCT_DATA_BATCH_SIZE = 100; +const MAX_PURCHASE_UPDATE_PAGES = 100; type VegaListener = (payload: any) => void; @@ -325,6 +327,14 @@ function productDataToArray( return Object.values(productData); } +function chunkArray(items: T[], size: number): T[][] { + const chunks: T[][] = []; + for (let index = 0; index < items.length; index += size) { + chunks.push(items.slice(index, index + size)); + } + return chunks; +} + function createPricingPhase(product: VegaProduct) { const price = product.price ?? {}; return { @@ -465,8 +475,17 @@ export function createExpoIapVegaModule( const receipts: VegaReceipt[] = []; let reset = true; let hasMore = false; + let pageCount = 0; do { + if (pageCount >= MAX_PURCHASE_UPDATE_PAGES) { + throw createVegaError( + ErrorCode.ServiceError, + 'Amazon Vega purchase updates exceeded the pagination limit.', + ); + } + pageCount++; + const response = await service.getPurchaseUpdates({reset}); ensureSuccessful( 'purchase-updates', @@ -482,6 +501,19 @@ export function createExpoIapVegaModule( return receipts; }; + const getProductData = async ( + skus: string[], + message: string, + ): Promise => { + const products: VegaProduct[] = []; + for (const batch of chunkArray(skus, MAX_PRODUCT_DATA_BATCH_SIZE)) { + const response = await service.getProductData({skus: batch}); + ensureSuccessful('product-data', response, message); + products.push(...productDataToArray(response.productData)); + } + return products; + }; + const hydrateProductTypesForReceipts = async ( receipts: VegaReceipt[], ): Promise => { @@ -499,16 +531,12 @@ export function createExpoIapVegaModule( if (missingSkus.size === 0) return; - const response = await service.getProductData({ - skus: Array.from(missingSkus), - }); - ensureSuccessful( - 'product-data', - response, + const products = await getProductData( + Array.from(missingSkus), 'Failed to fetch Amazon Vega product data for purchase updates', ); - for (const product of productDataToArray(response.productData)) { + for (const product of products) { if (product.sku) { productTypesBySku.set(product.sku, product.productType); } @@ -814,14 +842,12 @@ export function createExpoIapVegaModule( throw createVegaError(ErrorCode.EmptySkuList, 'No SKUs provided'); } - const response = await service.getProductData({skus}); - ensureSuccessful( - 'product-data', - response, + const products = await getProductData( + skus, 'Failed to fetch Amazon Vega products', ); - return productDataToArray(response.productData) + return products .filter((product) => { if (product.sku) { productTypesBySku.set(product.sku, product.productType); diff --git a/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts b/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts index ddf2dcad..1aa40710 100644 --- a/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts +++ b/libraries/react-native-iap/src/__tests__/vega-adapter.test.ts @@ -405,6 +405,82 @@ describe('Amazon Vega adapter', () => { ]); }); + it('chunks Vega product data requests', async () => { + const service = createService(); + const skus = Array.from({length: 101}, (_, index) => `sku_${index}`); + service.getProductData.mockImplementation(async ({skus: batch}) => ({ + responseCode: 1, + productData: Object.fromEntries( + batch.map((sku) => [ + sku, + { + sku, + title: sku, + description: 'Product', + productType: 1, + price: { + priceCurrencyCode: 'USD', + priceStr: '$0.99', + valueInMicros: 990000, + }, + }, + ]), + ), + })); + const module = createVegaIapModule(service); + + const products = await module.fetchProducts(skus, 'all'); + + expect(products).toHaveLength(101); + expect(service.getProductData).toHaveBeenCalledTimes(2); + expect(service.getProductData.mock.calls[0]?.[0].skus).toHaveLength(100); + expect(service.getProductData.mock.calls[1]?.[0].skus).toHaveLength(1); + }); + + it('chunks product type hydration for purchase updates', async () => { + const service = createService(); + const skus = Array.from({length: 101}, (_, index) => `sub_${index}`); + service.getPurchaseUpdates.mockResolvedValue({ + responseCode: 1, + receiptList: skus.map((sku) => ({ + receiptId: `receipt_${sku}`, + sku, + })), + }); + service.getProductData.mockImplementation(async ({skus: batch}) => ({ + responseCode: 1, + productData: Object.fromEntries( + batch.map((sku) => [ + sku, + { + sku, + title: sku, + description: 'Subscription', + productType: 3, + subscriptionPeriod: 'P1M', + price: { + priceCurrencyCode: 'USD', + priceStr: '$4.99', + valueInMicros: 4990000, + }, + }, + ]), + ), + })); + const module = createVegaIapModule(service); + + const purchases = await module.getAvailablePurchases(); + + expect(purchases).toHaveLength(101); + expect(purchases[0]).toMatchObject({ + isAutoRenewing: true, + productId: 'sub_0', + }); + expect(service.getProductData).toHaveBeenCalledTimes(2); + expect(service.getProductData.mock.calls[0]?.[0].skus).toHaveLength(100); + expect(service.getProductData.mock.calls[1]?.[0].skus).toHaveLength(1); + }); + it('excludes suspended purchases unless requested', async () => { const service = createService(); service.getPurchaseUpdates.mockResolvedValue({ diff --git a/libraries/react-native-iap/src/vega-adapter.ts b/libraries/react-native-iap/src/vega-adapter.ts index 8643db04..af6186e2 100644 --- a/libraries/react-native-iap/src/vega-adapter.ts +++ b/libraries/react-native-iap/src/vega-adapter.ts @@ -19,6 +19,7 @@ type ResponseOperation = const IAPKIT_VERIFY_TIMEOUT_MS = 10_000; const MAX_IAPKIT_ERROR_DEPTH = 5; +const MAX_PRODUCT_DATA_BATCH_SIZE = 100; const MAX_PURCHASE_UPDATE_PAGES = 100; interface VegaPrice { @@ -329,6 +330,14 @@ function productDataToArray( return Object.values(productData); } +function chunkArray(items: T[], size: number): T[][] { + const chunks: T[][] = []; + for (let index = 0; index < items.length; index += size) { + chunks.push(items.slice(index, index + size)); + } + return chunks; +} + function createPricingPhase(product: VegaProduct) { const price = product.price ?? {}; return { @@ -536,6 +545,19 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { return receipts; }; + const getProductData = async ( + skus: string[], + message: string, + ): Promise => { + const products: VegaProduct[] = []; + for (const batch of chunkArray(skus, MAX_PRODUCT_DATA_BATCH_SIZE)) { + const response = await service.getProductData({skus: batch}); + ensureSuccessful('product-data', response, message); + products.push(...productDataToArray(response.productData)); + } + return products; + }; + const hydrateProductTypesForReceipts = async ( receipts: VegaReceipt[], ): Promise => { @@ -553,16 +575,12 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { if (missingSkus.size === 0) return; - const response = await service.getProductData({ - skus: Array.from(missingSkus), - }); - ensureSuccessful( - 'product-data', - response, + const products = await getProductData( + Array.from(missingSkus), 'Failed to fetch Amazon Vega product data for purchase updates', ); - for (const product of productDataToArray(response.productData)) { + for (const product of products) { if (product.sku) { productTypesBySku.set(product.sku, product.productType); } @@ -879,14 +897,12 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { throw createVegaError(ErrorCode.EmptySkuList, 'No SKUs provided'); } - const response = await service.getProductData({skus}); - ensureSuccessful( - 'product-data', - response, + const products = await getProductData( + skus, 'Failed to fetch Amazon Vega products', ); - return productDataToArray(response.productData) + return products .filter((product) => { if (product.sku) { productTypesBySku.set(product.sku, product.productType); diff --git a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt index a628d016..c3b16497 100644 --- a/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/amazon/java/dev/hyo/openiap/OpenIapModule.kt @@ -334,8 +334,7 @@ class OpenIapModule( ) } val purchase = receipt.toPurchase() - purchaseTypeByReceiptId[receipt.receiptId] = receipt.productType - productTypeBySku[receipt.sku] = receipt.productType + cacheReceiptProductType(receipt, receipt.productTypeOrNull()) purchaseUpdateListeners.forEach { listener -> runCatching { listener.onPurchaseUpdated(purchase) } } @@ -683,7 +682,7 @@ class OpenIapModule( } private suspend fun requestPurchaseUpdates(reset: Boolean): List { - val purchases = mutableListOf() + val receipts = mutableListOf() var shouldReset = reset var pageCount = 0 do { @@ -697,13 +696,8 @@ class OpenIapModule( shouldReset = false when (response.requestStatus) { PurchaseUpdatesResponse.RequestStatus.SUCCESSFUL -> { - purchases += response.receipts.orEmpty() + receipts += response.receipts.orEmpty() .filter { it.cancelDate == null } - .map { receipt -> - purchaseTypeByReceiptId[receipt.receiptId] = receipt.productType - productTypeBySku[receipt.sku] = receipt.productType - receipt.toPurchase() - } } PurchaseUpdatesResponse.RequestStatus.NOT_SUPPORTED -> { throw OpenIapError.FeatureNotSupported("Amazon Appstore IAP is not supported on this device") @@ -713,7 +707,70 @@ class OpenIapModule( } } } while (response.hasMore()) - return purchases + hydrateProductTypesForReceipts(receipts) + return receipts.map { receipt -> + val productType = receipt.productTypeOrNull() + ?: productTypeBySku[receipt.sku.orEmpty()] + cacheReceiptProductType(receipt, productType) + receipt.toPurchase(productType) + } + } + + private suspend fun hydrateProductTypesForReceipts(receipts: List) { + val missingSkus = linkedSetOf() + receipts.forEach { receipt -> + val sku = receipt.sku.orEmpty() + if (sku.isBlank()) return@forEach + + val productType = receipt.productTypeOrNull() + if (productType != null) { + cacheReceiptProductType(receipt, productType) + } else if (!productTypeBySku.containsKey(sku)) { + missingSkus += sku + } + } + if (missingSkus.isEmpty()) return + + missingSkus.chunked(AMAZON_PRODUCT_DATA_BATCH_SIZE).forEach { batch -> + val response = requestProductData(batch) + when (response.requestStatus) { + ProductDataResponse.RequestStatus.SUCCESSFUL -> { + response.productData.orEmpty().values.forEach { product -> + productTypeBySku[product.sku] = product.productType + } + } + ProductDataResponse.RequestStatus.NOT_SUPPORTED -> { + throw OpenIapError.FeatureNotSupported("Amazon Appstore IAP is not supported on this device") + } + ProductDataResponse.RequestStatus.FAILED -> { + throw OpenIapError.QueryProduct.withDiagnostics( + debugMessage = "Amazon getProductData failed while hydrating purchase types", + productIds = batch, + productType = ProductQueryType.All.rawValue, + isEmptyProductList = response.productData.isNullOrEmpty() + ) + } + } + } + } + + private fun cacheReceiptProductType( + receipt: AmazonReceipt, + productType: AmazonProductType? + ) { + if (productType == null) return + val receiptId = receipt.receiptId.orEmpty() + val sku = receipt.sku.orEmpty() + if (receiptId.isNotBlank()) { + purchaseTypeByReceiptId[receiptId] = productType + } + if (sku.isNotBlank()) { + productTypeBySku[sku] = productType + } + } + + private fun AmazonReceipt.productTypeOrNull(): AmazonProductType? { + return runCatching { productType }.getOrNull() } private fun Throwable.toOpenIapError(defaultMessage: String): OpenIapError { @@ -894,8 +951,10 @@ class OpenIapModule( return AmazonPriceParser.toPriceAmount(this) } - private fun AmazonReceipt.toPurchase(): PurchaseAndroid { - val isSubscription = productType == AmazonProductType.SUBSCRIPTION + private fun AmazonReceipt.toPurchase( + productTypeOverride: AmazonProductType? = productTypeOrNull() + ): PurchaseAndroid { + val isSubscription = productTypeOverride == AmazonProductType.SUBSCRIPTION val dateMillis = purchaseDate?.time?.toDouble() ?: 0.0 val receiptCanceled = isCanceled || cancelDate != null val receiptDeferred = isDeferred From b2d905f660c9ff2f2f930543b5d25435d44744e2 Mon Sep 17 00:00:00 2001 From: hyochan Date: Tue, 26 May 2026 04:32:31 +0900 Subject: [PATCH 17/51] fix(iapkit): complete provider bridge parity --- knowledge/_claude-context/context.md | 53 +++- .../expo-iap/src/__tests__/useIAP.test.tsx | 61 +++++ libraries/expo-iap/src/useIAP.ts | 7 +- .../AndroidInappPurchasePlugin.kt | 14 + .../Classes/FlutterInappPurchasePlugin.swift | 11 + .../lib/flutter_inapp_purchase.dart | 65 ++++- .../Classes/FlutterInappPurchasePlugin.swift | 11 + .../flutter_inapp_purchase_channel_test.dart | 136 +++++++++- .../godot-iap/addons/godot-iap/godot_iap.gd | 54 +++- .../main/java/dev/hyo/godotiap/GodotIap.kt | 108 ++++---- .../main/java/dev/hyo/godotiap/GodotIapLog.kt | 51 ++++ .../Sources/GodotIap/GodotIap.swift | 18 +- .../Sources/GodotIap/GodotIapLog.swift | 27 +- .../hyochan/kmpiap/InAppPurchaseAndroid.kt | 18 +- .../github/hyochan/kmpiap/InAppPurchaseIOS.kt | 76 ++++-- .../dev/hyo/openiap/maui/OpenIapMauiModule.kt | 10 +- .../ApiDefinition.cs | 6 + .../OpenIap.Maui/Platforms/iOS/OpenIapIOS.cs | 17 +- .../react-native-iap/ios/HybridRnIap.swift | 12 + llms-full.txt | 14 +- llms.txt | 16 +- .../apple/Sources/OpenIapModule+ObjC.swift | 26 ++ packages/apple/Sources/OpenIapModule.swift | 55 ++-- packages/docs/public/llms-full.txt | 14 +- packages/docs/public/llms.txt | 16 +- .../src/pages/docs/foundation/sponsorship.tsx | 2 +- packages/docs/src/pages/docs/kit-backend.tsx | 2 +- .../utils/PurchaseVerificationValidator.kt | 240 +++++++++--------- .../PurchaseVerificationValidatorTest.kt | 177 +++++++++++++ packages/kit/public/llms-full.txt | 31 ++- packages/kit/public/llms.txt | 9 +- packages/kit/src/content/faq.md | 8 +- packages/kit/src/content/terms-of-service.md | 2 +- packages/kit/src/pages/blog/index.tsx | 2 +- packages/kit/src/pages/blog/posts.ts | 4 +- .../src/pages/docs/sections/introduction.tsx | 46 ++-- .../kit/src/pages/docs/sections/projects.tsx | 2 +- .../docs/sections/verification-apple.tsx | 1 + packages/kit/src/pages/landing.tsx | 8 +- 39 files changed, 1086 insertions(+), 344 deletions(-) diff --git a/knowledge/_claude-context/context.md b/knowledge/_claude-context/context.md index 2fa27cbe..0531f8a6 100644 --- a/knowledge/_claude-context/context.md +++ b/knowledge/_claude-context/context.md @@ -1,7 +1,7 @@ # OpenIAP Project Context > **Auto-generated for Claude Code** -> Last updated: 2026-05-18T04:52:45.916Z +> Last updated: 2026-05-23T13:40:17.719Z > > Usage: `claude --context knowledge/_claude-context/context.md` @@ -583,6 +583,35 @@ let userId = '123'; var config = { timeout: 5000 }; ``` +### Keep Single-Use Helpers Local + +Private helper functions used by only one function should be declared inside +that function so their scope matches their real ownership. Keep helpers at file +scope only when they are exported, reused by multiple call sites, or need a +stable top-level identity for tests, recursion, or platform registration. + +```typescript +// ✅ CORRECT - helper is owned by getResolved() +function getResolved(): ResolvedModule { + function getExpectedModuleName(): NativeModuleName { + return isVegaOS() ? 'ExpoIapVega' : 'ExpoIap'; + } + + const expectedName = getExpectedModuleName(); + return resolve(expectedName); +} + +// ❌ INCORRECT - helper has only one call site but lives at file scope +function getExpectedModuleName(): NativeModuleName { + return isVegaOS() ? 'ExpoIapVega' : 'ExpoIap'; +} + +function getResolved(): ResolvedModule { + const expectedName = getExpectedModuleName(); + return resolve(expectedName); +} +``` + ### Prefer Interface Over Type for Objects ```typescript @@ -861,6 +890,12 @@ The mechanical guardrail for this checklist is: bun run audit:parity ``` +This mirrors CI's **Audit SDK Parity** job and is intentionally run by the +pre-commit hook on every commit. Do not bypass it for docs/version-only changes: +the audit also checks generated docs version metadata and the Godot Android +GDAP dependency pin against `openiap-versions.json`, so release-version drift can +break CI even when no SDK source code changed. + This audit treats `libraries/expo-iap/example` as the non-Godot example SSOT and fails when: @@ -871,11 +906,17 @@ and fails when: - a GraphQL Query/Mutation/Subscription operation is added or removed without updating the operation parity registry - generated types or shared TS runtime helpers drift from `packages/gql` - -Run it after type generation and before opening a PR for SDK/API/example -changes. If it fails for a newly introduced operation or feature, update the -missing SDK bridge/example/test coverage first, then update the parity registry -in [`scripts/audit-non-godot-parity.mjs`](../../scripts/audit-non-godot-parity.mjs). +- framework/package version metadata or Godot Android GDAP dependencies drift + from the package/version SSOTs + +Run it after type generation, after version syncs, and before opening a PR for +SDK/API/example/docs-version changes. If it fails for a newly introduced +operation or feature, update the missing SDK bridge/example/test coverage first, +then update the parity registry in +[`scripts/audit-non-godot-parity.mjs`](../../scripts/audit-non-godot-parity.mjs). +If it fails for Godot GDAP dependency drift, run +`./libraries/godot-iap/scripts/write-gdap.sh` and commit the regenerated +`libraries/godot-iap/addons/godot-iap/android/GodotIap.gdap`. ### The bug pattern diff --git a/libraries/expo-iap/src/__tests__/useIAP.test.tsx b/libraries/expo-iap/src/__tests__/useIAP.test.tsx index c888f63c..8e59ba9c 100644 --- a/libraries/expo-iap/src/__tests__/useIAP.test.tsx +++ b/libraries/expo-iap/src/__tests__/useIAP.test.tsx @@ -14,6 +14,7 @@ jest.mock('react-native', () => ({ import * as React from 'react'; import * as ReactTestRenderer from 'react-test-renderer'; import ExpoIapModule from '../ExpoIapModule'; +import {ErrorCode} from '../types'; import {useIAP, UseIAPOptions} from '../useIAP'; /* eslint-enable import/first */ @@ -53,6 +54,7 @@ function TestComponent({ // Helper to wait for async operations const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0)); +const purchaseErrorEvent = 'purchase-error'; describe('useIAP hook', () => { beforeEach(() => { @@ -427,4 +429,63 @@ describe('useIAP hook', () => { expect(ExpoIapModule.initConnection).toHaveBeenCalled(); }); }); + + describe('purchase error listener', () => { + const renderWithCapturedListeners = async () => { + const listeners: Record void> = {}; + (ExpoIapModule.addListener as jest.Mock) = jest.fn( + (eventName: string, listener: (payload: any) => void) => { + listeners[eventName] = listener; + return {remove: jest.fn()}; + }, + ); + + await ReactTestRenderer.act(async () => { + ReactTestRenderer.create( {}} />); + await flushPromises(); + }); + + await ReactTestRenderer.act(async () => { + await flushPromises(); + }); + + return listeners; + }; + + it('does not warn for already-owned purchase errors', async () => { + const listeners = await renderWithCapturedListeners(); + consoleWarnSpy.mockClear(); + + listeners[purchaseErrorEvent]({ + code: ErrorCode.AlreadyOwned, + message: 'Already owned', + }); + + expect(consoleWarnSpy).not.toHaveBeenCalled(); + }); + + it('does not warn for service-timeout purchase errors', async () => { + const listeners = await renderWithCapturedListeners(); + consoleWarnSpy.mockClear(); + + listeners[purchaseErrorEvent]({ + code: ErrorCode.ServiceTimeout, + message: 'Service timeout', + }); + + expect(consoleWarnSpy).not.toHaveBeenCalled(); + }); + + it('warns for non-recoverable purchase errors', async () => { + const listeners = await renderWithCapturedListeners(); + consoleWarnSpy.mockClear(); + + listeners[purchaseErrorEvent]({ + code: ErrorCode.DeveloperError, + message: 'Developer error', + }); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/libraries/expo-iap/src/useIAP.ts b/libraries/expo-iap/src/useIAP.ts index abe03723..404f8778 100644 --- a/libraries/expo-iap/src/useIAP.ts +++ b/libraries/expo-iap/src/useIAP.ts @@ -645,7 +645,12 @@ export function useIAP(options?: UseIAPOptions): UseIap { return; // Ignore initialization error before connected } const friendly = getUserFriendlyErrorMessage(error); - if (!isUserCancelledError(error) && !isRecoverableError(error)) { + if ( + error.code !== ErrorCode.AlreadyOwned && + error.code !== ErrorCode.ServiceTimeout && + !isUserCancelledError(error) && + !isRecoverableError(error) + ) { ExpoIapConsole.warn('[useIAP] Purchase error:', friendly); } diff --git a/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt b/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt index decd2fea..7d916b9a 100644 --- a/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt +++ b/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt @@ -1195,6 +1195,20 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act ((iapkit["apple"] as? Map<*, *>)?.get("jws") as? String)?.let { jws -> iapkitMap["apple"] = mapOf("jws" to jws) } + (iapkit["amazon"] as? Map<*, *>)?.let { amazon -> + (amazon["receiptId"] as? String)?.let { receiptId -> + val amazonMap = mutableMapOf( + "receiptId" to receiptId + ) + (amazon["sandbox"] as? Boolean)?.let { sandbox -> + amazonMap["sandbox"] = sandbox + } + (amazon["userId"] as? String)?.let { userId -> + amazonMap["userId"] = userId + } + iapkitMap["amazon"] = amazonMap + } + } propsMap["iapkit"] = iapkitMap } diff --git a/libraries/flutter_inapp_purchase/ios/Classes/FlutterInappPurchasePlugin.swift b/libraries/flutter_inapp_purchase/ios/Classes/FlutterInappPurchasePlugin.swift index aa23951a..13dd09fc 100644 --- a/libraries/flutter_inapp_purchase/ios/Classes/FlutterInappPurchasePlugin.swift +++ b/libraries/flutter_inapp_purchase/ios/Classes/FlutterInappPurchasePlugin.swift @@ -934,6 +934,17 @@ public class FlutterInappPurchasePlugin: NSObject, FlutterPlugin { if let purchaseToken = (iapkit["google"] as? [String: Any])?["purchaseToken"] as? String { iapkitDict["google"] = ["purchaseToken": purchaseToken] } + if let amazon = iapkit["amazon"] as? [String: Any], + let receiptId = amazon["receiptId"] as? String { + var amazonDict: [String: Any] = ["receiptId": receiptId] + if let sandbox = amazon["sandbox"] as? Bool { + amazonDict["sandbox"] = sandbox + } + if let userId = amazon["userId"] as? String { + amazonDict["userId"] = userId + } + iapkitDict["amazon"] = amazonDict + } propsDict["iapkit"] = iapkitDict } diff --git a/libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart b/libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart index f6096b0f..9b05e4be 100644 --- a/libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart +++ b/libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart @@ -1919,6 +1919,14 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi { if (iapkit.apple != null) 'apple': {'jws': iapkit.apple!.jws}, if (iapkit.google != null) 'google': {'purchaseToken': iapkit.google!.purchaseToken}, + if (iapkit.amazon != null) + 'amazon': { + 'receiptId': iapkit.amazon!.receiptId, + if (iapkit.amazon!.sandbox != null) + 'sandbox': iapkit.amazon!.sandbox, + if (iapkit.amazon!.userId != null) + 'userId': iapkit.amazon!.userId, + }, }; } @@ -1951,22 +1959,51 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi { } // Parse iapkit result (single object, not array) + gentype.RequestVerifyPurchaseWithIapkitResult parseIapkitResult( + dynamic value) { + if (value is! Map) { + throw PurchaseError( + code: gentype.ErrorCode.PurchaseVerificationFailed, + message: + 'Malformed IAPKit verification result: iapkit must be an object', + ); + } + + final itemMap = value.map( + (key, value) => MapEntry(key.toString(), value), + ); + final isValid = itemMap['isValid']; + final state = itemMap['state']; + final store = itemMap['store']; + if (isValid is! bool || state == null || store == null) { + throw PurchaseError( + code: gentype.ErrorCode.PurchaseVerificationFailed, + message: + 'Malformed IAPKit verification result: missing isValid, state, or store', + ); + } + + return gentype.RequestVerifyPurchaseWithIapkitResult( + isValid: isValid, + state: gentype.IapkitPurchaseState.fromJson( + state.toString(), + ), + store: gentype.IapStore.fromJson(store.toString()), + ); + } + gentype.RequestVerifyPurchaseWithIapkitResult? iapkitResult; final iapkitData = resultMap['iapkit']; if (iapkitData != null) { - final itemMap = iapkitData is Map - ? iapkitData.map( - (k, v) => MapEntry(k.toString(), v), - ) - : {}; - iapkitResult = gentype.RequestVerifyPurchaseWithIapkitResult( - isValid: itemMap['isValid'] as bool? ?? false, - state: gentype.IapkitPurchaseState.fromJson( - itemMap['state']?.toString() ?? 'unknown', - ), - store: gentype.IapStore.fromJson( - itemMap['store']?.toString() ?? 'apple', - ), + iapkitResult = parseIapkitResult(iapkitData); + } + + final providerValue = resultMap['provider']; + if (providerValue == null) { + throw PurchaseError( + code: gentype.ErrorCode.PurchaseVerificationFailed, + message: + 'Malformed IAPKit verification result: missing provider', ); } @@ -1987,7 +2024,7 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi { iapkit: iapkitResult, errors: errors, provider: gentype.PurchaseVerificationProvider.fromJson( - resultMap['provider']?.toString() ?? 'iapkit', + providerValue.toString(), ), ); } on PlatformException catch (error) { diff --git a/libraries/flutter_inapp_purchase/macos/Classes/FlutterInappPurchasePlugin.swift b/libraries/flutter_inapp_purchase/macos/Classes/FlutterInappPurchasePlugin.swift index cf1318c5..63d7eaf7 100644 --- a/libraries/flutter_inapp_purchase/macos/Classes/FlutterInappPurchasePlugin.swift +++ b/libraries/flutter_inapp_purchase/macos/Classes/FlutterInappPurchasePlugin.swift @@ -799,6 +799,17 @@ public class FlutterInappPurchasePlugin: NSObject, FlutterPlugin { if let purchaseToken = (iapkit["google"] as? [String: Any])?["purchaseToken"] as? String { iapkitDict["google"] = ["purchaseToken": purchaseToken] } + if let amazon = iapkit["amazon"] as? [String: Any], + let receiptId = amazon["receiptId"] as? String { + var amazonDict: [String: Any] = ["receiptId": receiptId] + if let sandbox = amazon["sandbox"] as? Bool { + amazonDict["sandbox"] = sandbox + } + if let userId = amazon["userId"] as? String { + amazonDict["userId"] = userId + } + iapkitDict["amazon"] = amazonDict + } propsDict["iapkit"] = iapkitDict } diff --git a/libraries/flutter_inapp_purchase/test/flutter_inapp_purchase_channel_test.dart b/libraries/flutter_inapp_purchase/test/flutter_inapp_purchase_channel_test.dart index ed5557f9..c2812ed2 100644 --- a/libraries/flutter_inapp_purchase/test/flutter_inapp_purchase_channel_test.dart +++ b/libraries/flutter_inapp_purchase/test/flutter_inapp_purchase_channel_test.dart @@ -2188,6 +2188,70 @@ void main() { expect(result.iapkit!.store, types.IapStore.Google); }); + test('sends correct payload for Amazon verification', () async { + final calls = []; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall call) async { + calls.add(call); + switch (call.method) { + case 'initConnection': + return true; + case 'verifyPurchaseWithProvider': + return jsonEncode({ + 'provider': 'iapkit', + 'iapkit': { + 'isValid': true, + 'state': 'entitled', + 'store': 'amazon', + }, + }); + } + return null; + }); + + final iap = FlutterInappPurchase.private( + FakePlatform(operatingSystem: 'android'), + ); + + await iap.initConnection(); + + final result = await iap.verifyPurchaseWithProvider( + provider: types.PurchaseVerificationProvider.Iapkit, + iapkit: const types.RequestVerifyPurchaseWithIapkitProps( + apiKey: 'test-api-key', + amazon: types.RequestVerifyPurchaseWithIapkitAmazonProps( + receiptId: 'amzn1.receipt.test', + sandbox: true, + userId: 'amzn1.account.test', + ), + ), + ); + + final verifyCall = calls.singleWhere( + (MethodCall c) => c.method == 'verifyPurchaseWithProvider', + ); + final payload = Map.from( + verifyCall.arguments as Map, + ); + + expect(payload['provider'], 'iapkit'); + final iapkitPayload = Map.from( + payload['iapkit'] as Map, + ); + expect(iapkitPayload['amazon'], isNotNull); + final amazonPayload = Map.from( + iapkitPayload['amazon'] as Map, + ); + expect(amazonPayload['receiptId'], 'amzn1.receipt.test'); + expect(amazonPayload['sandbox'], true); + expect(amazonPayload['userId'], 'amzn1.account.test'); + + expect(result.iapkit, isNotNull); + expect(result.iapkit!.isValid, true); + expect(result.iapkit!.state, types.IapkitPurchaseState.Entitled); + expect(result.iapkit!.store, types.IapStore.Amazon); + }); + test('throws PurchaseError on platform exception', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall call) async { @@ -2264,6 +2328,50 @@ void main() { expect(result.iapkit, isNull); }); + test('rejects missing provider in response', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall call) async { + switch (call.method) { + case 'initConnection': + return true; + case 'verifyPurchaseWithProvider': + return { + 'iapkit': { + 'isValid': true, + 'state': 'entitled', + 'store': 'apple', + }, + }; + } + return null; + }); + + final iap = FlutterInappPurchase.private( + FakePlatform(operatingSystem: 'ios'), + ); + + await iap.initConnection(); + + await expectLater( + iap.verifyPurchaseWithProvider( + provider: types.PurchaseVerificationProvider.Iapkit, + iapkit: const types.RequestVerifyPurchaseWithIapkitProps( + apiKey: 'test-api-key', + apple: types.RequestVerifyPurchaseWithIapkitAppleProps( + jws: 'test-jws-token', + ), + ), + ), + throwsA( + isA().having( + (error) => error.code, + 'code', + types.ErrorCode.PurchaseVerificationFailed, + ), + ), + ); + }); + test('handles errors in response', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall call) async { @@ -2317,7 +2425,7 @@ void main() { expect(result.errors![1].message, 'Subscription has expired'); }); - test('handles iapkit as non-Map gracefully', () async { + test('rejects iapkit as non-Map', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall call) async { switch (call.method) { @@ -2338,20 +2446,24 @@ void main() { await iap.initConnection(); - final result = await iap.verifyPurchaseWithProvider( - provider: types.PurchaseVerificationProvider.Iapkit, - iapkit: const types.RequestVerifyPurchaseWithIapkitProps( - apiKey: 'test-api-key', - apple: types.RequestVerifyPurchaseWithIapkitAppleProps( - jws: 'test-jws-token', + await expectLater( + iap.verifyPurchaseWithProvider( + provider: types.PurchaseVerificationProvider.Iapkit, + iapkit: const types.RequestVerifyPurchaseWithIapkitProps( + apiKey: 'test-api-key', + apple: types.RequestVerifyPurchaseWithIapkitAppleProps( + jws: 'test-jws-token', + ), + ), + ), + throwsA( + isA().having( + (error) => error.code, + 'code', + types.ErrorCode.PurchaseVerificationFailed, ), ), ); - - // Should handle gracefully with default values - expect(result.provider, types.PurchaseVerificationProvider.Iapkit); - expect(result.iapkit, isNotNull); - expect(result.iapkit!.isValid, false); // Default value }); }); } diff --git a/libraries/godot-iap/addons/godot-iap/godot_iap.gd b/libraries/godot-iap/addons/godot-iap/godot_iap.gd index 32baa822..80684f7e 100644 --- a/libraries/godot-iap/addons/godot-iap/godot_iap.gd +++ b/libraries/godot-iap/addons/godot-iap/godot_iap.gd @@ -667,17 +667,27 @@ func get_storefront() -> String: ## See: https://openiap.dev/docs/features/validation#verify-purchase func verify_purchase(props) -> Variant: print("[GodotIap] verify_purchase called") - var result = _verify_purchase_raw(props.to_dict()) + var props_dict: Dictionary = props.to_dict() if props is Object and props.has_method("to_dict") else (props if props is Dictionary else {}) + if _native_plugin and _platform == "iOS": + var pending = _native_plugin.call("verifyPurchase", JSON.stringify(props_dict)) + var request_id = _parse_request_id(pending) + var payload = await _await_products_fetched_for("verifyPurchase", request_id) + if payload.get("success", false): + var payload_json = payload.get("resultJson", "") + var decoded = JSON.parse_string(payload_json) + if decoded is Dictionary: + return Types.VerifyPurchaseResultIOS.from_dict(decoded) + return null + + var result = _verify_purchase_raw(props_dict) if result.get("success", false) or result.get("isValid", false): - if _platform == "iOS": - return Types.VerifyPurchaseResultIOS.from_dict(result) - elif _platform == "Android": + if _platform == "Android": return Types.VerifyPurchaseResultAndroid.from_dict(result) return null ## Internal: Verify purchase with raw Dictionary func _verify_purchase_raw(props: Dictionary) -> Dictionary: - if _native_plugin and (_platform == "Android" or _platform == "iOS"): + if _native_plugin and _platform == "Android": var props_json = JSON.stringify(props) var result_json = _native_plugin.call("verifyPurchase", props_json) var result = JSON.parse_string(result_json) @@ -693,19 +703,47 @@ func _verify_purchase_raw(props: Dictionary) -> Dictionary: ## See: https://openiap.dev/docs/features/validation#verify-purchase-with-provider func verify_purchase_with_provider(props) -> Variant: print("[GodotIap] verify_purchase_with_provider called") - var result = _verify_purchase_with_provider_raw(props.to_dict()) + var props_dict: Dictionary = props.to_dict() if props is Object and props.has_method("to_dict") else (props if props is Dictionary else {}) + if _native_plugin and _platform == "iOS": + var pending = _native_plugin.call("verifyPurchaseWithProvider", JSON.stringify(props_dict)) + var request_id = _parse_request_id(pending) + var payload = await _await_products_fetched_for("verifyPurchaseWithProvider", request_id) + if payload.get("success", false): + var payload_json = payload.get("resultJson", "") + var decoded = JSON.parse_string(payload_json) + if decoded is Dictionary: + return Types.VerifyPurchaseWithProviderResult.from_dict(decoded) + return Types.VerifyPurchaseWithProviderResult.from_dict({ + "provider": props_dict.get("provider", "iapkit"), + "errors": [ + { + "code": "purchase-verification-failed", + "message": payload.get("error", "Verification failed"), + }, + ], + }) + + var result = _verify_purchase_with_provider_raw(props_dict) return Types.VerifyPurchaseWithProviderResult.from_dict(result) ## Internal: Verify purchase with provider raw Dictionary func _verify_purchase_with_provider_raw(props: Dictionary) -> Dictionary: - if _native_plugin and (_platform == "Android" or _platform == "iOS"): + if _native_plugin and _platform == "Android": var props_json = JSON.stringify(props) var result_json = _native_plugin.call("verifyPurchaseWithProvider", props_json) var result = JSON.parse_string(result_json) if result is Dictionary: return result # No native plugin - return { "success": false, "isValid": false, "error": "Not available in no native plugin" } + return { + "provider": props.get("provider", "iapkit"), + "errors": [ + { + "code": "feature-not-supported", + "message": "Not available in no native plugin", + }, + ], + } # ========================================== diff --git a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt index ac8af7dd..b895f0da 100644 --- a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt +++ b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt @@ -922,68 +922,90 @@ class GodotIap(godot: Godot) : GodotPlugin(godot) { @UsedByGodot fun verifyPurchaseWithProvider(propsJson: String): String { - GodotIapLog.payload("verifyPurchaseWithProvider", propsJson) + GodotIapLog.payload( + "verifyPurchaseWithProvider", + mapOf("hasIapkit" to propsJson.contains("\"iapkit\"")) + ) - if (!isInitialized) { + fun errorResponse(message: String): String { return JSONObject().apply { put("success", false) - put("error", "Not initialized") + put("provider", PurchaseVerificationProvider.Iapkit.toJson()) + put( + "errors", + JSONArray().apply { + put(JSONObject().apply { + put("code", ErrorCode.PurchaseVerificationFailed.toJson()) + put("message", message) + }) + } + ) }.toString() } + if (!isInitialized) { + return errorResponse("Not initialized") + } + return runBlocking { try { - val json = JSONObject(propsJson) + val jsonBridge = object { + fun objectToMap(json: JSONObject): Map { + val map = linkedMapOf() + val keys = json.keys() + while (keys.hasNext()) { + val key = keys.next() + map[key] = valueToAny(json.opt(key)) + } + return map + } - // Build IAPKit props - val iapkitProps = RequestVerifyPurchaseWithIapkitProps( - apiKey = json.optString("apiKey").takeIf { it.isNotEmpty() }, - apple = json.optJSONObject("apple")?.let { appleJson -> - RequestVerifyPurchaseWithIapkitAppleProps( - jws = appleJson.getString("jws") - ) - }, - google = json.optJSONObject("google")?.let { googleJson -> - RequestVerifyPurchaseWithIapkitGoogleProps( - purchaseToken = googleJson.getString("purchaseToken") - ) + fun arrayToList(json: JSONArray): List { + return (0 until json.length()).map { index -> + valueToAny(json.opt(index)) + } } - ) - val providerProps = VerifyPurchaseWithProviderProps( - iapkit = iapkitProps, - provider = PurchaseVerificationProvider.Iapkit - ) + fun valueToAny(value: Any?): Any? { + if (value == null || value == JSONObject.NULL) return null + return when (value) { + is JSONObject -> objectToMap(value) + is JSONArray -> arrayToList(value) + else -> value + } + } + } - val result = openIap.verifyPurchaseWithProvider(providerProps) + fun normalizeProviderProps(props: Map): Map { + if (props["iapkit"] != null) return props - val response = JSONObject().apply { - put("success", true) - put("provider", result.provider.toJson()) - result.iapkit?.let { iapkit -> - put("isValid", iapkit.isValid) - put("state", iapkit.state.toJson()) - put("store", iapkit.store.toJson()) - } - result.errors?.let { errors -> - val errorsArray = JSONArray() - errors.forEach { error -> - errorsArray.put(JSONObject().apply { - put("code", error.code) - put("message", error.message) - }) + val legacyIapkit = linkedMapOf() + listOf("amazon", "apiKey", "apple", "google").forEach { key -> + if (props[key] != null) { + legacyIapkit[key] = props[key] } - put("errors", errorsArray) } - }.toString() + if (legacyIapkit.isEmpty()) return props + + return linkedMapOf( + "provider" to (props["provider"] ?: PurchaseVerificationProvider.Iapkit.toJson()), + "iapkit" to legacyIapkit + ) + } + + val propsMap = normalizeProviderProps(jsonBridge.objectToMap(JSONObject(propsJson))) + val providerProps = VerifyPurchaseWithProviderProps.fromJson(propsMap) + ?: throw IllegalArgumentException("Invalid verifyPurchaseWithProvider options") + + val result = openIap.verifyPurchaseWithProvider(providerProps) + val response = JSONObject(GodotIapHelper.sanitizeDictionary(result.toJson())) + .apply { put("success", true) } + .toString() GodotIapLog.result("verifyPurchaseWithProvider", "verified") response } catch (e: Exception) { GodotIapLog.failure("verifyPurchaseWithProvider", e) - JSONObject().apply { - put("success", false) - put("error", e.message) - }.toString() + errorResponse(e.message ?: "Verification failed") } } } diff --git a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapLog.kt b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapLog.kt index 013ef307..5d2cad52 100644 --- a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapLog.kt +++ b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapLog.kt @@ -70,6 +70,7 @@ internal object GodotIapLog { if (value == null) return null return when (value) { + is String -> sanitizeJsonString(value) is Map<*, *> -> sanitizeMap(value) is List<*> -> value.mapNotNull { sanitize(it) } is Array<*> -> value.mapNotNull { sanitize(it) } @@ -89,4 +90,54 @@ internal object GodotIapLog { } return sanitized } + + private fun sanitizeJsonString(value: String): Any { + val trimmed = value.trim() + return try { + when { + trimmed.startsWith("{") -> sanitizeJsonObject(JSONObject(trimmed)) + trimmed.startsWith("[") -> sanitizeJsonArray(JSONArray(trimmed)) + else -> value + } + } catch (_: Exception) { + value + } + } + + private fun sanitizeJsonObject(source: JSONObject): Map { + val sanitized = linkedMapOf() + val keys = source.keys() + while (keys.hasNext()) { + val key = keys.next() + sanitized[key] = + if (isSensitiveKey(key)) { + "hidden" + } else { + sanitizeJsonValue(source.opt(key)) + } + } + return sanitized + } + + private fun sanitizeJsonArray(source: JSONArray): List { + return (0 until source.length()).mapNotNull { index -> + sanitizeJsonValue(source.opt(index)) + } + } + + private fun sanitizeJsonValue(value: Any?): Any? { + if (value == null || value == JSONObject.NULL) return null + return when (value) { + is JSONObject -> sanitizeJsonObject(value) + is JSONArray -> sanitizeJsonArray(value) + else -> sanitize(value) + } + } + + private fun isSensitiveKey(key: String): Boolean { + val normalized = key.lowercase().filter { it.isLetterOrDigit() } + return listOf("token", "apikey", "secret", "jws", "receiptid", "userid").any { + normalized.contains(it) + } + } } diff --git a/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIap.swift b/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIap.swift index a9428f0f..8eb03985 100644 --- a/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIap.swift +++ b/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIap.swift @@ -968,6 +968,7 @@ public class GodotIap: RefCounted, @unchecked Sendable { @Callable public func verifyPurchase(propsJson: String) -> String { GodotIapLog.payload("Verifying purchase", payload: nil) + let requestId = UUID().uuidString Task { [weak self] in guard let self = self else { return } @@ -976,6 +977,8 @@ public class GodotIap: RefCounted, @unchecked Sendable { let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { await MainActor.run { [self] in let dict = VariantDictionary() + dict["method"] = Variant("verifyPurchase") + dict["requestId"] = Variant(requestId) dict["success"] = Variant(false) dict["error"] = Variant("Invalid arguments") self.productsFetched.emit(dict) @@ -989,6 +992,8 @@ public class GodotIap: RefCounted, @unchecked Sendable { await MainActor.run { [self] in let dict = VariantDictionary() + dict["method"] = Variant("verifyPurchase") + dict["requestId"] = Variant(requestId) dict["success"] = Variant(true) let resultDict = OpenIapSerialization.encode(result) if let jsonData = try? JSONSerialization.data(withJSONObject: resultDict), @@ -1001,6 +1006,8 @@ public class GodotIap: RefCounted, @unchecked Sendable { GodotIapLog.debug("[GodotIap] verifyPurchase error: \(error.localizedDescription)") await MainActor.run { [self] in let dict = VariantDictionary() + dict["method"] = Variant("verifyPurchase") + dict["requestId"] = Variant(requestId) dict["success"] = Variant(false) dict["error"] = Variant(error.localizedDescription) self.productsFetched.emit(dict) @@ -1008,7 +1015,7 @@ public class GodotIap: RefCounted, @unchecked Sendable { } } - return "{\"status\": \"pending\"}" + return "{\"status\": \"pending\", \"requestId\": \"\(requestId)\"}" } @Callable @@ -1080,6 +1087,7 @@ public class GodotIap: RefCounted, @unchecked Sendable { @Callable public func verifyPurchaseWithProvider(propsJson: String) -> String { GodotIapLog.payload("Verifying purchase with provider", payload: nil) + let requestId = UUID().uuidString Task { [weak self] in guard let self = self else { return } @@ -1088,6 +1096,8 @@ public class GodotIap: RefCounted, @unchecked Sendable { let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { await MainActor.run { [self] in let dict = VariantDictionary() + dict["method"] = Variant("verifyPurchaseWithProvider") + dict["requestId"] = Variant(requestId) dict["success"] = Variant(false) dict["error"] = Variant("Invalid arguments") self.productsFetched.emit(dict) @@ -1102,6 +1112,8 @@ public class GodotIap: RefCounted, @unchecked Sendable { await MainActor.run { [self] in let dict = VariantDictionary() + dict["method"] = Variant("verifyPurchaseWithProvider") + dict["requestId"] = Variant(requestId) dict["success"] = Variant(true) let resultDict = OpenIapSerialization.encode(result) if let jsonData = try? JSONSerialization.data(withJSONObject: resultDict), @@ -1114,6 +1126,8 @@ public class GodotIap: RefCounted, @unchecked Sendable { GodotIapLog.debug("[GodotIap] verifyPurchaseWithProvider error: \(error.localizedDescription)") await MainActor.run { [self] in let dict = VariantDictionary() + dict["method"] = Variant("verifyPurchaseWithProvider") + dict["requestId"] = Variant(requestId) dict["success"] = Variant(false) dict["error"] = Variant(error.localizedDescription) self.productsFetched.emit(dict) @@ -1121,7 +1135,7 @@ public class GodotIap: RefCounted, @unchecked Sendable { } } - return "{\"status\": \"pending\"}" + return "{\"status\": \"pending\", \"requestId\": \"\(requestId)\"}" } // MARK: - StoreKit 2 Deprecated / Alias APIs diff --git a/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIapLog.swift b/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIapLog.swift index 076770d4..91ae4512 100644 --- a/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIapLog.swift +++ b/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIapLog.swift @@ -130,6 +130,10 @@ enum GodotIapLog { private static func sanitize(_ value: Any?) -> Any? { guard let value else { return nil } + if let string = value as? String { + return sanitizeJSONString(string) + } + if let dictionary = value as? [String: Any] { return sanitizeDictionary(dictionary) } @@ -158,7 +162,7 @@ enum GodotIapLog { private static func sanitizeDictionary(_ dictionary: [String: Any]) -> [String: Any] { var sanitized: [String: Any] = [:] for (key, value) in dictionary { - if key.lowercased().contains("token") { + if isSensitiveKey(key) { sanitized[key] = "hidden" } else if let sanitizedValue = sanitize(value) { sanitized[key] = sanitizedValue @@ -166,4 +170,25 @@ enum GodotIapLog { } return sanitized } + + private static func sanitizeJSONString(_ value: String) -> Any { + let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) + guard trimmed.first == "{" || trimmed.first == "[" else { + return value + } + + guard let data = trimmed.data(using: .utf8), + let json = try? JSONSerialization.jsonObject(with: data) else { + return value + } + + return sanitize(json) ?? value + } + + private static func isSensitiveKey(_ key: String) -> Bool { + let normalized = key.lowercased() + .filter { $0.isLetter || $0.isNumber } + let sensitiveFragments = ["token", "apikey", "secret", "jws", "receiptid", "userid"] + return sensitiveFragments.contains { normalized.contains($0) } + } } diff --git a/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt b/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt index 3e21680d..19de6885 100644 --- a/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt +++ b/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt @@ -1152,12 +1152,20 @@ internal class InAppPurchaseAndroid : KmpInAppPurchase, Application.ActivityLife message = "IAPKit options are required for Android verification" ) ) - val googleOptions = iapkitOptions.google ?: failWith( - PurchaseError( - code = ErrorCode.PurchaseVerificationFailed, - message = "Google purchaseToken is required for Android verification" + val payloadCount = listOfNotNull( + iapkitOptions.apple, + iapkitOptions.google, + iapkitOptions.amazon + ).size + val googleOptions = iapkitOptions.google + if (payloadCount != 1 || googleOptions == null) { + failWith( + PurchaseError( + code = ErrorCode.PurchaseVerificationFailed, + message = "IAPKit verification on KMP Android requires exactly one google payload" + ) ) - ) + } return try { val openIapProps = GoogleVerifyPurchaseWithIapkitProps( diff --git a/libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt b/libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt index 0ab9a9f2..ca4b4922 100644 --- a/libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt +++ b/libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt @@ -967,11 +967,35 @@ internal class InAppPurchaseIOS : KmpInAppPurchase { * * @see https://openiap.dev/docs/features/validation#verify-purchase-with-provider */ - override suspend fun verifyPurchaseWithProvider(options: VerifyPurchaseWithProviderProps): VerifyPurchaseWithProviderResult = - suspendCoroutine { continuation -> + override suspend fun verifyPurchaseWithProvider(options: VerifyPurchaseWithProviderProps): VerifyPurchaseWithProviderResult { + if (options.provider != PurchaseVerificationProvider.Iapkit) { + throw PurchaseException( + PurchaseError( + code = ErrorCode.FeatureNotSupported, + message = "Verification provider ${options.provider.rawValue} is not supported on iOS" + ) + ) + } + val iapkit = options.iapkit ?: throw PurchaseException( + PurchaseError( + code = ErrorCode.PurchaseVerificationFailed, + message = "IAPKit options are required for iOS verification" + ) + ) + val payloadCount = listOfNotNull(iapkit.apple, iapkit.google, iapkit.amazon).size + if (payloadCount != 1 || iapkit.apple == null) { + throw PurchaseException( + PurchaseError( + code = ErrorCode.PurchaseVerificationFailed, + message = "IAPKit verification on KMP iOS requires exactly one apple payload" + ) + ) + } + + return suspendCoroutine { continuation -> val provider = options.provider.rawValue - val apiKey = options.iapkit?.apiKey - val jws = options.iapkit?.apple?.jws + val apiKey = iapkit.apiKey + val jws = iapkit.apple.jws openIapModule.verifyPurchaseWithProviderObjCWithProvider( provider = provider, @@ -1005,30 +1029,25 @@ internal class InAppPurchaseIOS : KmpInAppPurchase { try { val map = (result as? Map<*, *>)?.mapKeys { it.key.toString() } - - val iapkitResult = if (map != null) { - val isValid = map["isValid"] as? Boolean ?: false - val stateString = map["state"] as? String ?: "unknown" - val storeString = map["store"] as? String ?: "apple" - - val state = try { - IapkitPurchaseState.fromJson(stateString) - } catch (e: IllegalArgumentException) { - IapkitPurchaseState.Unknown - } - - val store = try { - IapStore.fromJson(storeString) - } catch (e: IllegalArgumentException) { - IapStore.Apple - } - - RequestVerifyPurchaseWithIapkitResult( - isValid = isValid, - state = state, - store = store - ) - } else null + ?: throw IllegalArgumentException("IAPKit result must be an object") + val isValid = map["isValid"] as? Boolean + ?: throw IllegalArgumentException("IAPKit result missing isValid") + val stateString = map["state"] as? String + ?: throw IllegalArgumentException("IAPKit result missing state") + val storeString = map["store"] as? String + ?: throw IllegalArgumentException("IAPKit result missing store") + val state = runCatching { + IapkitPurchaseState.fromJson(stateString) + }.getOrDefault(IapkitPurchaseState.Unknown) + val store = IapStore.fromJson(storeString) + if (store != IapStore.Apple) { + throw IllegalArgumentException("IAPKit result store mismatch: $storeString") + } + val iapkitResult = RequestVerifyPurchaseWithIapkitResult( + isValid = isValid, + state = state, + store = store + ) continuation.resume( VerifyPurchaseWithProviderResult( @@ -1048,6 +1067,7 @@ internal class InAppPurchaseIOS : KmpInAppPurchase { } } } + } // ------------------------------------------------------------------------- // SubscriptionResolver Implementation diff --git a/libraries/maui-iap/android/openiap/src/main/java/dev/hyo/openiap/maui/OpenIapMauiModule.kt b/libraries/maui-iap/android/openiap/src/main/java/dev/hyo/openiap/maui/OpenIapMauiModule.kt index 7826bb9e..38c5c013 100644 --- a/libraries/maui-iap/android/openiap/src/main/java/dev/hyo/openiap/maui/OpenIapMauiModule.kt +++ b/libraries/maui-iap/android/openiap/src/main/java/dev/hyo/openiap/maui/OpenIapMauiModule.kt @@ -115,7 +115,7 @@ class OpenIapMauiModule(context: Context) { // ----------------------------------------------------------------- fun fetchProducts(paramsJson: String, callback: ResultCallback) = run(callback) { - val params = ProductRequest.fromJson(parseMap(paramsJson)) ?: throw badInput("ProductRequest", paramsJson) + val params = ProductRequest.fromJson(parseMap(paramsJson)) ?: throw badInput("ProductRequest") encodeFetchProductsResult(module.fetchProducts(params)) } @@ -183,7 +183,7 @@ class OpenIapMauiModule(context: Context) { fun verifyPurchaseWithProvider(propsJson: String, callback: ResultCallback) = run(callback) { val props = VerifyPurchaseWithProviderProps.fromJson(parseMap(propsJson)) - ?: throw badInput("VerifyPurchaseWithProviderProps", propsJson) + ?: throw badInput("VerifyPurchaseWithProviderProps") gson.toJson(module.verifyPurchaseWithProvider(props).toJson()) } @@ -224,7 +224,7 @@ class OpenIapMauiModule(context: Context) { fun launchExternalLinkAndroid(paramsJson: String, callback: ResultCallback) = run(callback) { val params = LaunchExternalLinkParamsAndroid.fromJson(parseMap(paramsJson)) - ?: throw badInput("LaunchExternalLinkParamsAndroid", paramsJson) + ?: throw badInput("LaunchExternalLinkParamsAndroid") val activity = currentActivityOrThrow("launchExternalLinkAndroid") wrapBool(module.launchExternalLink(activity, params)) } @@ -431,9 +431,9 @@ class OpenIapMauiModule(context: Context) { return gson.toJson(payload) } - private fun badInput(typeName: String, json: String): IllegalArgumentException { + private fun badInput(typeName: String): IllegalArgumentException { // The generated `Type.fromJson(Map)` returns null when required fields are missing. // Surface this as a typed error so the C# side can map it to OpenIapError.DeveloperError. - return IllegalArgumentException("Could not decode $typeName from JSON: $json") + return IllegalArgumentException("Could not decode $typeName from JSON") } } diff --git a/libraries/maui-iap/src/OpenIap.Maui.Bindings.iOS/ApiDefinition.cs b/libraries/maui-iap/src/OpenIap.Maui.Bindings.iOS/ApiDefinition.cs index fb06f43b..f230f0fa 100644 --- a/libraries/maui-iap/src/OpenIap.Maui.Bindings.iOS/ApiDefinition.cs +++ b/libraries/maui-iap/src/OpenIap.Maui.Bindings.iOS/ApiDefinition.cs @@ -124,6 +124,12 @@ void VerifyPurchaseWithProvider( [NullAllowed] string jws, Action completion); + [Export("verifyPurchaseWithProviderObjCWithPayload:completion:")] + [Async] + void VerifyPurchaseWithProviderPayload( + NSDictionary payload, + Action completion); + // -- Storefront / Subscriptions ---------------------------------------- [Export("getStorefrontWithCompletion:")] diff --git a/libraries/maui-iap/src/OpenIap.Maui/Platforms/iOS/OpenIapIOS.cs b/libraries/maui-iap/src/OpenIap.Maui/Platforms/iOS/OpenIapIOS.cs index 37613607..ddd239bb 100644 --- a/libraries/maui-iap/src/OpenIap.Maui/Platforms/iOS/OpenIapIOS.cs +++ b/libraries/maui-iap/src/OpenIap.Maui/Platforms/iOS/OpenIapIOS.cs @@ -372,22 +372,19 @@ public Task VerifyPurchaseAsync(VerifyPurchaseProps option public Task VerifyPurchaseWithProviderAsync(VerifyPurchaseWithProviderProps options) { - var providerEnum = options.Provider; - var apiKey = options.Iapkit?.ApiKey; - var jws = options.Iapkit?.Apple?.Jws; + var payloadNode = JsonSerializer.SerializeToNode(options, JsonOptions.Default) as JsonObject + ?? throw OpenIapErrorMapper.Wrap(ErrorCode.DeveloperError, "Invalid verifyPurchaseWithProvider payload"); + var payload = NSObjectJsonBridge.JsonObjectToDictionary(payloadNode); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _module.VerifyPurchaseWithProvider(providerEnum.ToJson(), apiKey, jws, (dict, err) => + _module.VerifyPurchaseWithProviderPayload(payload, (dict, err) => { try { if (err is not null) { tcs.TrySetException(MapNSError(err)); return; } var node = NSObjectJsonBridge.DictToObject(dict); - var typed = node?.Deserialize(JsonOptions.Default); - tcs.TrySetResult(new VerifyPurchaseWithProviderResult - { - Provider = providerEnum, - Iapkit = typed, - }); + var typed = node?.Deserialize(JsonOptions.Default); + if (typed is not null) tcs.TrySetResult(typed); + else tcs.TrySetException(OpenIapErrorMapper.Wrap(ErrorCode.Unknown, "verifyPurchaseWithProvider returned no payload")); } catch (Exception ex) { tcs.TrySetException(ex); } }); diff --git a/libraries/react-native-iap/ios/HybridRnIap.swift b/libraries/react-native-iap/ios/HybridRnIap.swift index 14034b81..76dc7aab 100644 --- a/libraries/react-native-iap/ios/HybridRnIap.swift +++ b/libraries/react-native-iap/ios/HybridRnIap.swift @@ -443,6 +443,18 @@ class HybridRnIap: HybridRnIapSpec { if case .second(let google) = iapkit.google { iapkitDict["google"] = ["purchaseToken": google.purchaseToken] } + if case .second(let amazon) = iapkit.amazon { + var amazonDict: [String: Any] = [ + "receiptId": amazon.receiptId + ] + if case .second(let sandbox) = amazon.sandbox { + amazonDict["sandbox"] = sandbox + } + if case .second(let userId) = amazon.userId { + amazonDict["userId"] = userId + } + iapkitDict["amazon"] = amazonDict + } propsDict["iapkit"] = iapkitDict } // Use JSONSerialization + JSONDecoder like expo-iap does diff --git a/llms-full.txt b/llms-full.txt index cfabebcc..9919036c 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -3,7 +3,7 @@ > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Quick Reference: https://openiap.dev/llms.txt -> Generated: 2026-05-18T04:52:45.925Z +> Generated: 2026-05-23T13:40:17.731Z ## Table of Contents 1. Installation @@ -40,13 +40,13 @@ pod 'openiap', '~> 2.2.1' ### Kotlin (Android) ```kotlin // Gradle (build.gradle.kts) -implementation("io.github.hyochan.openiap:openiap-google:2.2.0") +implementation("io.github.hyochan.openiap:openiap-google:2.3.0-rc.1") // For Meta Horizon OS -implementation("io.github.hyochan.openiap:openiap-google-horizon:2.2.0") +implementation("io.github.hyochan.openiap:openiap-google-horizon:2.3.0-rc.1") // For Fire OS (Amazon Appstore) -implementation("io.github.hyochan.openiap:openiap-google-amazon:2.2.0") +implementation("io.github.hyochan.openiap:openiap-google-amazon:2.3.0-rc.1") ``` ### Flutter @@ -55,13 +55,13 @@ flutter pub add flutter_inapp_purchase ``` ### Godot -Download `godot-iap-2.3.0.zip` from GitHub Releases, extract it to +Download `godot-iap-2.4.0-rc.1.zip` from GitHub Releases, extract it to `addons/godot-iap/`, then enable the plugin in Project Settings. ### Kotlin Multiplatform ```kotlin dependencies { - implementation("io.github.hyochan:kmp-iap:2.3.0") + implementation("io.github.hyochan:kmp-iap:2.4.0-rc.1") } ``` @@ -73,7 +73,7 @@ https://central.sonatype.com/artifact/io.github.hyochan/kmp-iap dotnet add package OpenIap.Maui ``` -Current NuGet package version: 1.1.0 +Current NuGet package version: 1.2.0-rc.1 Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. diff --git a/llms.txt b/llms.txt index ed6f0981..dd8231b6 100644 --- a/llms.txt +++ b/llms.txt @@ -3,7 +3,7 @@ > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Full Reference: https://openiap.dev/llms-full.txt -> Generated: 2026-05-18T04:52:45.925Z +> Generated: 2026-05-23T13:40:17.731Z ## Installation @@ -24,9 +24,9 @@ npm install react-native-iap ```kotlin // Gradle -implementation("io.github.hyochan.openiap:openiap-google:2.2.0") -implementation("io.github.hyochan.openiap:openiap-google-horizon:2.2.0") -implementation("io.github.hyochan.openiap:openiap-google-amazon:2.2.0") +implementation("io.github.hyochan.openiap:openiap-google:2.3.0-rc.1") +implementation("io.github.hyochan.openiap:openiap-google-horizon:2.3.0-rc.1") +implementation("io.github.hyochan.openiap:openiap-google-amazon:2.3.0-rc.1") ``` ```bash @@ -36,20 +36,20 @@ flutter pub add flutter_inapp_purchase ```gdscript # Godot -# Install godot-iap 2.3.0 to addons/godot-iap and enable the plugin +# Install godot-iap 2.4.0-rc.1 to addons/godot-iap and enable the plugin ``` ```kotlin // Kotlin Multiplatform -implementation("io.github.hyochan:kmp-iap:2.3.0") +implementation("io.github.hyochan:kmp-iap:2.4.0-rc.1") ``` ```xml - + ``` -Current NuGet package version: 1.1.0 +Current NuGet package version: 1.2.0-rc.1 ## Framework Libraries diff --git a/packages/apple/Sources/OpenIapModule+ObjC.swift b/packages/apple/Sources/OpenIapModule+ObjC.swift index ef2b2d7e..a94dacb4 100644 --- a/packages/apple/Sources/OpenIapModule+ObjC.swift +++ b/packages/apple/Sources/OpenIapModule+ObjC.swift @@ -547,6 +547,32 @@ import StoreKit } } + /// Verify purchase with external provider using the generated OpenIAP payload shape. + /// - Parameters: + /// - payload: VerifyPurchaseWithProviderProps encoded as NSDictionary + /// - completion: Callback with full verification result dictionary or error + @objc(verifyPurchaseWithProviderObjCWithPayload:completion:) + func verifyPurchaseWithProviderPayloadObjC( + payload: NSDictionary, + completion: @escaping ([String: Any]?, Error?) -> Void + ) { + Task { + do { + guard JSONSerialization.isValidJSONObject(payload) else { + throw PurchaseError(code: .developerError, message: "Invalid verification payload") + } + + let data = try JSONSerialization.data(withJSONObject: payload, options: []) + let props = try JSONDecoder().decode(VerifyPurchaseWithProviderProps.self, from: data) + let result = try await verifyPurchaseWithProvider(props) + let dictionary = OpenIapSerialization.encode(result) + completion(dictionary, nil) + } catch { + completion(nil, error) + } + } + } + // MARK: - Store Information @objc func getStorefrontWithCompletion(_ completion: @escaping (String?, Error?) -> Void) { diff --git a/packages/apple/Sources/OpenIapModule.swift b/packages/apple/Sources/OpenIapModule.swift index d7ae2b28..2bab5de9 100644 --- a/packages/apple/Sources/OpenIapModule.swift +++ b/packages/apple/Sources/OpenIapModule.swift @@ -721,24 +721,32 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } func extractIapkitErrorMessage(from json: [String: Any]) -> String? { - if let details = json["details"] as? [String: Any], - let originalError = details["originalError"] as? String { - if let data = originalError.data(using: .utf8), + func extractStringMessage(_ value: String) -> String { + if let data = value.data(using: .utf8), let nested = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { - return extractIapkitErrorMessage(from: nested) ?? originalError + return extractIapkitErrorMessage(from: nested) ?? value } - return originalError + return value + } + + if let details = json["details"] as? [String: Any], + let originalError = details["originalError"] as? String { + return extractStringMessage(originalError) } if let errors = json["errors"] as? [[String: Any]], let firstError = errors.first { return extractIapkitErrorMessage(from: firstError) } - if let message = json["message"] as? String, !message.contains("{\"error\"") { - return message + if let message = json["message"] as? String { + return extractStringMessage(message) } - return json["error"] as? String + if let error = json["error"] as? String { + return extractStringMessage(error) + } + + return nil } func buildIapkitPayload(props: RequestVerifyPurchaseWithIapkitProps) throws -> Data { @@ -760,8 +768,9 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { func verifyPurchaseWithIapkit(props: RequestVerifyPurchaseWithIapkitProps) async throws -> RequestVerifyPurchaseWithIapkitResult { let url = URL(string: "https://kit.openiap.dev/v1/purchase/verify")! - guard props.apple != nil else { - throw makePurchaseError(code: .developerError, message: "IAPKit verification on Apple requires an apple payload") + let payloadCount = [props.apple != nil, props.google != nil, props.amazon != nil].filter { $0 }.count + guard payloadCount == 1, props.apple != nil else { + throw makePurchaseError(code: .developerError, message: "IAPKit verification on Apple requires exactly one apple payload") } let store: IapStore = .apple let body = try buildIapkitPayload(props: props) @@ -769,7 +778,8 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") - if let apiKey = props.apiKey, apiKey.isEmpty == false { + let apiKey = props.apiKey?.trimmingCharacters(in: .whitespacesAndNewlines) + if let apiKey, apiKey.isEmpty == false { request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") } request.httpBody = body @@ -777,7 +787,14 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { OpenIapLog.debug("IAPKit request URL: \(url.absoluteString)") OpenIapLog.debug("IAPKit request body bytes=\(body.count)") - let (data, response) = try await URLSession.shared.data(for: request) + let data: Data + let response: URLResponse + do { + (data, response) = try await URLSession.shared.data(for: request) + } catch { + OpenIapLog.warn("IAPKit verification network error: \(error.localizedDescription)") + throw makePurchaseError(code: .networkError, message: error.localizedDescription) + } guard let httpResponse = response as? HTTPURLResponse else { throw makePurchaseError(code: .networkError, message: "Invalid response") } @@ -800,18 +817,22 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } if let errors = json["errors"] as? [[String: Any]], let firstError = errors.first { - let errorMessage = firstError["message"] as? String ?? "Unknown error" + let errorMessage = extractIapkitErrorMessage(from: firstError) ?? "Unknown error" let errorCode = firstError["code"] as? String ?? "unknown" OpenIapLog.warn("IAPKit verification error: \(errorCode) - \(errorMessage)") throw makePurchaseError(code: .receiptFailed, message: errorMessage) } - let isValid = (json["isValid"] as? Bool) ?? false - let stateString = json["state"] as? String ?? "UNKNOWN" + guard let isValid = json["isValid"] as? Bool, + let stateString = json["state"] as? String, + let storeString = json["store"] as? String, + let parsedStore = IapStore(rawValue: storeString), + parsedStore == store else { + OpenIapLog.warn("IAPKit verification response missing required fields") + throw makePurchaseError(code: .receiptFailed, message: "IAPKit returned malformed response") + } let normalizedState = stateString.lowercased().replacingOccurrences(of: "_", with: "-") let parsedState = IapkitPurchaseState(rawValue: normalizedState) ?? .unknown - let storeString = json["store"] as? String - let parsedStore = storeString.flatMap { IapStore(rawValue: $0) } ?? store OpenIapLog.info("IAPKit verification result: store=\(parsedStore.rawValue), isValid=\(isValid), state=\(parsedState.rawValue)") return RequestVerifyPurchaseWithIapkitResult(isValid: isValid, state: parsedState, store: parsedStore) } diff --git a/packages/docs/public/llms-full.txt b/packages/docs/public/llms-full.txt index cfabebcc..9919036c 100644 --- a/packages/docs/public/llms-full.txt +++ b/packages/docs/public/llms-full.txt @@ -3,7 +3,7 @@ > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Quick Reference: https://openiap.dev/llms.txt -> Generated: 2026-05-18T04:52:45.925Z +> Generated: 2026-05-23T13:40:17.731Z ## Table of Contents 1. Installation @@ -40,13 +40,13 @@ pod 'openiap', '~> 2.2.1' ### Kotlin (Android) ```kotlin // Gradle (build.gradle.kts) -implementation("io.github.hyochan.openiap:openiap-google:2.2.0") +implementation("io.github.hyochan.openiap:openiap-google:2.3.0-rc.1") // For Meta Horizon OS -implementation("io.github.hyochan.openiap:openiap-google-horizon:2.2.0") +implementation("io.github.hyochan.openiap:openiap-google-horizon:2.3.0-rc.1") // For Fire OS (Amazon Appstore) -implementation("io.github.hyochan.openiap:openiap-google-amazon:2.2.0") +implementation("io.github.hyochan.openiap:openiap-google-amazon:2.3.0-rc.1") ``` ### Flutter @@ -55,13 +55,13 @@ flutter pub add flutter_inapp_purchase ``` ### Godot -Download `godot-iap-2.3.0.zip` from GitHub Releases, extract it to +Download `godot-iap-2.4.0-rc.1.zip` from GitHub Releases, extract it to `addons/godot-iap/`, then enable the plugin in Project Settings. ### Kotlin Multiplatform ```kotlin dependencies { - implementation("io.github.hyochan:kmp-iap:2.3.0") + implementation("io.github.hyochan:kmp-iap:2.4.0-rc.1") } ``` @@ -73,7 +73,7 @@ https://central.sonatype.com/artifact/io.github.hyochan/kmp-iap dotnet add package OpenIap.Maui ``` -Current NuGet package version: 1.1.0 +Current NuGet package version: 1.2.0-rc.1 Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. diff --git a/packages/docs/public/llms.txt b/packages/docs/public/llms.txt index ed6f0981..dd8231b6 100644 --- a/packages/docs/public/llms.txt +++ b/packages/docs/public/llms.txt @@ -3,7 +3,7 @@ > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Full Reference: https://openiap.dev/llms-full.txt -> Generated: 2026-05-18T04:52:45.925Z +> Generated: 2026-05-23T13:40:17.731Z ## Installation @@ -24,9 +24,9 @@ npm install react-native-iap ```kotlin // Gradle -implementation("io.github.hyochan.openiap:openiap-google:2.2.0") -implementation("io.github.hyochan.openiap:openiap-google-horizon:2.2.0") -implementation("io.github.hyochan.openiap:openiap-google-amazon:2.2.0") +implementation("io.github.hyochan.openiap:openiap-google:2.3.0-rc.1") +implementation("io.github.hyochan.openiap:openiap-google-horizon:2.3.0-rc.1") +implementation("io.github.hyochan.openiap:openiap-google-amazon:2.3.0-rc.1") ``` ```bash @@ -36,20 +36,20 @@ flutter pub add flutter_inapp_purchase ```gdscript # Godot -# Install godot-iap 2.3.0 to addons/godot-iap and enable the plugin +# Install godot-iap 2.4.0-rc.1 to addons/godot-iap and enable the plugin ``` ```kotlin // Kotlin Multiplatform -implementation("io.github.hyochan:kmp-iap:2.3.0") +implementation("io.github.hyochan:kmp-iap:2.4.0-rc.1") ``` ```xml - + ``` -Current NuGet package version: 1.1.0 +Current NuGet package version: 1.2.0-rc.1 ## Framework Libraries diff --git a/packages/docs/src/pages/docs/foundation/sponsorship.tsx b/packages/docs/src/pages/docs/foundation/sponsorship.tsx index df6794d9..7e95b887 100644 --- a/packages/docs/src/pages/docs/foundation/sponsorship.tsx +++ b/packages/docs/src/pages/docs/foundation/sponsorship.tsx @@ -111,7 +111,7 @@ function Sponsorship() { Platform policy response - When Apple or Google change billing APIs, we update the spec so + When source stores change billing APIs, we update the spec so you don't scramble diff --git a/packages/docs/src/pages/docs/kit-backend.tsx b/packages/docs/src/pages/docs/kit-backend.tsx index 615e8f37..0595a2bc 100644 --- a/packages/docs/src/pages/docs/kit-backend.tsx +++ b/packages/docs/src/pages/docs/kit-backend.tsx @@ -141,7 +141,7 @@ function KitBackend() {
  • Webhooks — copyable lifecycle webhook URL, the SSE stream URL, and a curl recipe for emitting a synthetic test - notification without going through the App Store / Play Console. + notification without opening a store console.
  • diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt index 243befaf..c79edc87 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt @@ -4,6 +4,7 @@ import com.google.gson.Gson import com.google.gson.JsonSyntaxException import com.google.gson.reflect.TypeToken import dev.hyo.openiap.IapStore +import dev.hyo.openiap.IapkitPurchaseState import dev.hyo.openiap.OpenIapError import dev.hyo.openiap.OpenIapLog import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitProps @@ -168,20 +169,98 @@ suspend fun verifyPurchaseWithIapkit( tag: String, connectionFactory: (String) -> HttpURLConnection = ::openConnection ): RequestVerifyPurchaseWithIapkitResult = withContext(Dispatchers.IO) { + fun malformedIapkitResponse(): OpenIapError.PurchaseVerificationFailed = + OpenIapError.PurchaseVerificationFailed("IAPKit returned malformed response") + val endpoint = DEFAULT_IAPKIT_ENDPOINT + val hasApple = props.apple != null val hasGoogle = props.google != null val hasAmazon = props.amazon != null - if (listOf(hasGoogle, hasAmazon).count { it } != 1) { + if (listOf(hasApple, hasGoogle, hasAmazon).count { it } != 1 || hasApple) { throw IllegalArgumentException( "IAPKit verification on Android requires exactly one google or amazon payload" ) } + fun buildGooglePayload(): Map { + val google = props.google + ?: throw IllegalArgumentException("IAPKit Google verification requires google options") + if (google.purchaseToken.isBlank()) { + throw IllegalArgumentException("IAPKit Google verification requires purchaseToken") + } + return mutableMapOf( + "store" to IapStore.Google.rawValue, + "purchaseToken" to google.purchaseToken + ) + } + + fun buildAmazonPayload(): Map { + val amazon = props.amazon + ?: throw IllegalArgumentException("IAPKit Amazon verification requires amazon options") + val userId = amazon.userId?.trim().orEmpty() + val receiptId = amazon.receiptId.trim() + if (userId.isBlank() || receiptId.isBlank()) { + throw IllegalArgumentException("IAPKit Amazon verification requires userId and receiptId") + } + return mutableMapOf( + "store" to IapStore.Amazon.rawValue, + "userId" to userId, + "receiptId" to receiptId + ).apply { + amazon.sandbox?.let { put("sandbox", it) } + } + } + + @Suppress("UNCHECKED_CAST") + fun extractIapkitErrorMessage(json: Map): String? { + fun extractStringMessage(value: String): String { + return try { + val nested = gson.fromJson(value, Map::class.java) as? Map + if (nested != null) { + extractIapkitErrorMessage(nested) ?: value + } else { + value + } + } catch (e: Exception) { + value + } + } + + val errorsRaw = json["errors"] + if (errorsRaw is List<*>) { + val firstError = errorsRaw.firstOrNull() + if (firstError is Map<*, *>) { + return extractIapkitErrorMessage(firstError as Map) + } + } + + val detailsRaw = json["details"] + if (detailsRaw is Map<*, *>) { + val details = detailsRaw as Map + val originalError = details["originalError"] + if (originalError is String) { + return extractStringMessage(originalError) + } + } + + val message = json["message"] as? String + if (message != null) { + return extractStringMessage(message) + } + + val error = json["error"] as? String + if (error != null) { + return extractStringMessage(error) + } + + return null + } + val store = if (hasAmazon) IapStore.Amazon else IapStore.Google val payload = when (store) { - IapStore.Amazon -> buildAmazonPayload(props) - IapStore.Google -> buildGooglePayload(props) + IapStore.Amazon -> buildAmazonPayload() + IapStore.Google -> buildGooglePayload() else -> throw IllegalArgumentException("IAPKit verification on Android does not support ${store.rawValue}") } @@ -196,6 +275,47 @@ suspend fun verifyPurchaseWithIapkit( try { val body = gson.toJson(payload) + val mapType = object : TypeToken>() {}.type + + fun parseIapkitObject(responseBody: String): Map { + return try { + gson.fromJson>(responseBody, mapType) + ?: throw malformedIapkitResponse() + } catch (error: JsonSyntaxException) { + OpenIapLog.warn("Failed to parse IAPKit verification response: ${error.message}", tag) + throw OpenIapError.PurchaseVerificationFailed("Failed to parse response") + } + } + + fun readIapkitResult(parsed: Map): RequestVerifyPurchaseWithIapkitResult { + val errorsRaw = parsed["errors"] + if (errorsRaw is List<*> && errorsRaw.isNotEmpty()) { + val errorMessage = extractIapkitErrorMessage(parsed) ?: "IAPKit verification failed" + throw OpenIapError.PurchaseVerificationFailed(errorMessage) + } + + val isValid = parsed["isValid"] as? Boolean + ?: throw malformedIapkitResponse() + val state = parsed["state"] as? String + ?: throw malformedIapkitResponse() + val responseStore = (parsed["store"] as? String) + ?.let { runCatching { IapStore.fromJson(it) }.getOrNull() } + ?: throw malformedIapkitResponse() + if (responseStore != store) { + throw malformedIapkitResponse() + } + + val normalizedState = state.lowercase().replace("_", "-") + val parsedState = runCatching { + IapkitPurchaseState.fromJson(normalizedState) + }.getOrDefault(IapkitPurchaseState.Unknown) + + return RequestVerifyPurchaseWithIapkitResult( + isValid = isValid, + state = parsedState, + store = responseStore + ) + } connection.outputStream.use { stream -> stream.write(body.toByteArray(Charsets.UTF_8)) @@ -212,7 +332,6 @@ suspend fun verifyPurchaseWithIapkit( // Extract concise error message from IAPKit response // IAPKit returns nested error format - extract the deepest originalError val errorMessage = try { - val mapType = object : TypeToken>() {}.type val errorJson = gson.fromJson>(responseBody, mapType) extractIapkitErrorMessage(errorJson) ?: "HTTP $statusCode" } catch (e: Exception) { @@ -221,122 +340,13 @@ suspend fun verifyPurchaseWithIapkit( throw OpenIapError.PurchaseVerificationFailed(errorMessage) } - try { - val mapType = object : TypeToken>() {}.type - val parsed = gson.fromJson>(responseBody, mapType) - // IAPKit API returns UPPER_SNAKE_CASE (e.g., "PURCHASED", "PENDING_ACKNOWLEDGMENT") - // Types.kt expects lower-kebab-case (e.g., "purchased", "pending-acknowledgment") - val normalizedParsed = parsed.toMutableMap().apply { - val state = this["state"] as? String - if (state != null) { - this["state"] = state.lowercase().replace("_", "-") - } - // IAPKit response doesn't include store, add it from request - if (this["store"] == null) { - this["store"] = store.toJson() - } - } - RequestVerifyPurchaseWithIapkitResult.fromJson(normalizedParsed) - } catch (jsonError: Exception) { - OpenIapLog.warn("Failed to parse IAPKit verification response: ${jsonError.message}", tag) - throw OpenIapError.PurchaseVerificationFailed("Failed to parse response") - } + readIapkitResult(parseIapkitObject(responseBody)) } catch (io: IOException) { OpenIapLog.warn("Network error during IAPKit verification: ${io.message}", tag) - throw OpenIapError.PurchaseVerificationFailed("Network error: ${io.message}") + throw OpenIapError.NetworkError } finally { connection.disconnect() } } -/** - * Build payload for Google Play Store verification via IAPKit. - */ -private fun buildGooglePayload(props: RequestVerifyPurchaseWithIapkitProps): Map { - val google = props.google - ?: throw IllegalArgumentException("IAPKit Google verification requires google options") - if (google.purchaseToken.isBlank()) { - throw IllegalArgumentException("IAPKit Google verification requires purchaseToken") - } - return mutableMapOf( - "store" to IapStore.Google.rawValue, - "purchaseToken" to google.purchaseToken - ) -} - -/** - * Build payload for Amazon Appstore RVS verification via IAPKit. - */ -private fun buildAmazonPayload(props: RequestVerifyPurchaseWithIapkitProps): Map { - val amazon = props.amazon - ?: throw IllegalArgumentException("IAPKit Amazon verification requires amazon options") - val userId = amazon.userId?.trim().orEmpty() - val receiptId = amazon.receiptId.trim() - if (userId.isBlank() || receiptId.isBlank()) { - throw IllegalArgumentException("IAPKit Amazon verification requires userId and receiptId") - } - return mutableMapOf( - "store" to IapStore.Amazon.rawValue, - "userId" to userId, - "receiptId" to receiptId - ).apply { - amazon.sandbox?.let { put("sandbox", it) } - } -} - - private fun String?.orElse(fallback: String): String = this ?: fallback - -/** - * Extract concise error message from IAPKit error response. - * IAPKit returns nested error structures - we extract the deepest originalError for clarity. - * - * Example input: - * {"error":"PLAY_STORE_VERIFICATION_ERROR","message":"Failed to verify Google Play purchase: {...}", - * "details":{"originalError":"..."}} - * - * Returns: "The purchase token is no longer valid." (the deepest originalError) - */ -@Suppress("UNCHECKED_CAST") -private fun extractIapkitErrorMessage(json: Map): String? { - // Try errors array format first: { "errors": [{ "code": "...", "message": "..." }] } - val errorsRaw = json["errors"] - if (errorsRaw is List<*>) { - val firstError = errorsRaw.firstOrNull() - if (firstError is Map<*, *>) { - val errorMap = firstError as Map - // Recursively extract from first error - return extractIapkitErrorMessage(errorMap) - } - } - - // Try to get details.originalError (deepest level) - val detailsRaw = json["details"] - if (detailsRaw is Map<*, *>) { - val details = detailsRaw as Map - val originalError = details["originalError"] - if (originalError is String) { - // originalError might be a JSON string, try to parse it - return try { - val nested = gson.fromJson(originalError, Map::class.java) as? Map - if (nested != null) { - extractIapkitErrorMessage(nested) ?: originalError - } else { - originalError - } - } catch (e: Exception) { - // Not JSON, use as-is - originalError - } - } - } - - // Try message field, but avoid the verbose nested JSON string - val message = json["message"] as? String - if (message != null && !message.contains("{\"error\"")) { - return message - } - - // Fallback to error code - return json["error"] as? String -} diff --git a/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt b/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt index b5864f9f..ddba7c26 100644 --- a/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt +++ b/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt @@ -7,6 +7,7 @@ import dev.hyo.openiap.utils.verifyPurchaseWithIapkit import dev.hyo.openiap.IapStore import dev.hyo.openiap.IapkitPurchaseState import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitAmazonProps +import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitAppleProps import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitGoogleProps import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitProps import dev.hyo.openiap.VerifyPurchaseGoogleOptions @@ -14,6 +15,7 @@ import dev.hyo.openiap.VerifyPurchaseHorizonOptions import dev.hyo.openiap.VerifyPurchaseProps import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream +import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.net.HttpURLConnection @@ -327,6 +329,50 @@ class PurchaseVerificationValidatorTest { } } + @Test + fun `verifyPurchaseWithIapkit throws when apple payload is provided on Android`() = runTest { + val props = RequestVerifyPurchaseWithIapkitProps( + apiKey = null, + apple = RequestVerifyPurchaseWithIapkitAppleProps( + jws = "header.payload.signature" + ), + google = null, + amazon = null + ) + + try { + verifyPurchaseWithIapkit(props, "TEST") { _ -> + throw AssertionError("Connection should not be created when apple payload is provided") + } + throw AssertionError("Expected IllegalArgumentException for apple payload on Android") + } catch (expected: IllegalArgumentException) { + assertTrue(expected.message?.contains("exactly one google or amazon") == true) + } + } + + @Test + fun `verifyPurchaseWithIapkit throws when apple is mixed with android payloads`() = runTest { + val props = RequestVerifyPurchaseWithIapkitProps( + apiKey = null, + apple = RequestVerifyPurchaseWithIapkitAppleProps( + jws = "header.payload.signature" + ), + google = RequestVerifyPurchaseWithIapkitGoogleProps( + purchaseToken = "token-123" + ), + amazon = null + ) + + try { + verifyPurchaseWithIapkit(props, "TEST") { _ -> + throw AssertionError("Connection should not be created when apple payload is mixed") + } + throw AssertionError("Expected IllegalArgumentException for mixed apple payload") + } catch (expected: IllegalArgumentException) { + assertTrue(expected.message?.contains("exactly one google or amazon") == true) + } + } + @Test fun `verifyPurchaseWithIapkit wraps non-2xx as PurchaseVerificationFailed`() = runTest { val props = RequestVerifyPurchaseWithIapkitProps( @@ -350,6 +396,125 @@ class PurchaseVerificationValidatorTest { } } + @Test + fun `verifyPurchaseWithIapkit extracts nested JSON error messages`() = runTest { + val props = RequestVerifyPurchaseWithIapkitProps( + apiKey = null, + apple = null, + google = RequestVerifyPurchaseWithIapkitGoogleProps( + purchaseToken = "token-123" + ), + amazon = null + ) + + try { + verifyPurchaseWithIapkit( + props, + "TEST" + ) { + FakeHttpURLConnection( + 400, + """{"message":"{\"error\":\"receipt no longer valid\"}"}""" + ) + } + throw AssertionError("Expected PurchaseVerificationFailed for non-2xx response") + } catch (error: OpenIapError.PurchaseVerificationFailed) { + assertTrue(error.message.contains("receipt no longer valid")) + } + } + + @Test + fun `verifyPurchaseWithIapkit rejects successful error payloads`() = runTest { + val props = RequestVerifyPurchaseWithIapkitProps( + apiKey = null, + apple = null, + google = RequestVerifyPurchaseWithIapkitGoogleProps( + purchaseToken = "token-123" + ), + amazon = null + ) + + try { + verifyPurchaseWithIapkit( + props, + "TEST" + ) { _ -> FakeHttpURLConnection(200, """{"errors":[{"message":"bad receipt"}]}""") } + throw AssertionError("Expected PurchaseVerificationFailed for IAPKit error payload") + } catch (error: OpenIapError.PurchaseVerificationFailed) { + assertTrue(error.message.contains("bad receipt")) + } + } + + @Test + fun `verifyPurchaseWithIapkit rejects missing result fields`() = runTest { + val props = RequestVerifyPurchaseWithIapkitProps( + apiKey = null, + apple = null, + google = null, + amazon = RequestVerifyPurchaseWithIapkitAmazonProps( + userId = "amzn1.account.ABC123", + receiptId = "amzn1.receipt.ABC123456789", + sandbox = true + ) + ) + + try { + verifyPurchaseWithIapkit( + props, + "TEST" + ) { _ -> FakeHttpURLConnection(200, """{"store":"amazon","state":"ENTITLED"}""") } + throw AssertionError("Expected PurchaseVerificationFailed for missing fields") + } catch (error: OpenIapError.PurchaseVerificationFailed) { + assertTrue(error.message.contains("malformed")) + } + } + + @Test + fun `verifyPurchaseWithIapkit rejects mismatched result store`() = runTest { + val props = RequestVerifyPurchaseWithIapkitProps( + apiKey = null, + apple = null, + google = null, + amazon = RequestVerifyPurchaseWithIapkitAmazonProps( + userId = "amzn1.account.ABC123", + receiptId = "amzn1.receipt.ABC123456789", + sandbox = true + ) + ) + + try { + verifyPurchaseWithIapkit( + props, + "TEST" + ) { _ -> FakeHttpURLConnection(200, """{"store":"google","isValid":true,"state":"ENTITLED"}""") } + throw AssertionError("Expected PurchaseVerificationFailed for mismatched store") + } catch (error: OpenIapError.PurchaseVerificationFailed) { + assertTrue(error.message.contains("malformed")) + } + } + + @Test + fun `verifyPurchaseWithIapkit wraps IO failures as NetworkError`() = runTest { + val props = RequestVerifyPurchaseWithIapkitProps( + apiKey = null, + apple = null, + google = RequestVerifyPurchaseWithIapkitGoogleProps( + purchaseToken = "token-123" + ), + amazon = null + ) + + try { + verifyPurchaseWithIapkit( + props, + "TEST" + ) { _ -> FailingHttpURLConnection() } + throw AssertionError("Expected NetworkError for IO failure") + } catch (error: OpenIapError.NetworkError) { + assertTrue(true) + } + } + // ===== Horizon verification tests ===== @Test @@ -442,3 +607,15 @@ private class FakeHttpURLConnection( override fun connect() { /* no-op */ } } + +private class FailingHttpURLConnection : HttpURLConnection(URL("https://example.com")) { + override fun getOutputStream(): OutputStream { + throw IOException("network offline") + } + + override fun disconnect() { /* no-op */ } + + override fun usingProxy(): Boolean = false + + override fun connect() { /* no-op */ } +} diff --git a/packages/kit/public/llms-full.txt b/packages/kit/public/llms-full.txt index 4273c7e1..e7326203 100644 --- a/packages/kit/public/llms-full.txt +++ b/packages/kit/public/llms-full.txt @@ -38,7 +38,8 @@ Mounted at both `/v1/*` (canonical) and `/api/v1/*` (alias). ### POST /v1/purchase/verify Verify an in-app purchase. The body is a tagged union discriminated on -`store`. Response always has shape `{ isValid: boolean, state: }`. +`store`. Response always has shape +`{ store: "apple" | "google" | "horizon" | "amazon", isValid: boolean, state: }`. Request (Apple): ```json @@ -60,6 +61,16 @@ stores it as a write-only credential and the server composes the `OC|APP_ID|APP_SECRET` access token when calling `POST https://graph.oculus.com/{APP_ID}/verify_entitlement`. +Request (Amazon Appstore): +```json +{ + "store": "amazon", + "userId": "amzn1.account...", + "receiptId": "amzn1.receipt...", + "sandbox": true +} +``` + ### Aliases - `POST /v1/verify-purchase` — identical handler, kept for the path @@ -97,14 +108,14 @@ probe traffic doesn't dominate trace quota. ## Status codes -| Code | Body | When | -| ---- | ------------------------------- | ------------------------------------------------- | -| 200 | `{ isValid, state }` | Verification ran | -| 400 | `INVALID_INPUT` | Malformed body, unknown store, oversized input | -| 401 | `MISSING_API_KEY` | No `Authorization` header | -| 403 | `INVALID_API_KEY` | Wrong scheme, malformed, or unknown key | -| 429 | `RATE_LIMITED` | Per-key bucket empty; honor `Retry-After` | -| 500 | `UNKNOWN_ERROR` | Server-side failure; include `X-Correlation-Id` | +| Code | Body | When | +| ---- | ------------------------------------- | ------------------------------------------------- | +| 200 | `{ store, isValid, state }` | Verification ran | +| 400 | `INVALID_INPUT` | Malformed body, unknown store, oversized input | +| 401 | `MISSING_API_KEY` | No `Authorization` header | +| 403 | `INVALID_API_KEY` | Wrong scheme, malformed, or unknown key | +| 429 | `RATE_LIMITED` | Per-key bucket empty; honor `Retry-After` | +| 500 | `UNKNOWN_ERROR` | Server-side failure; include `X-Correlation-Id` | Error body shape: ```json @@ -153,7 +164,7 @@ eviction (`RATE_LIMIT_MAX_STORE`). Oversized fields return `400 INVALID_INPUT`; oversized request bodies return `413 PAYLOAD_TOO_LARGE`. Neither path calls the upstream store, so a -misbehaving client can't burn Apple / Google / Meta quota. +misbehaving client can't burn Apple / Google / Horizon / Amazon quota. ## Structured logs diff --git a/packages/kit/public/llms.txt b/packages/kit/public/llms.txt index 2f2c090f..8512f678 100644 --- a/packages/kit/public/llms.txt +++ b/packages/kit/public/llms.txt @@ -1,8 +1,8 @@ # IAPKit > Receipt-validation SaaS managed by OpenIAP. Hosted at https://kit.openiap.dev. -> One Bearer-authed endpoint for all three stores (Apple / Google / Horizon); -> harmonized response shape so your backend has a single code path for +> One Bearer-authed endpoint for Apple / Google / Horizon / Amazon; +> harmonized response shape with `{ store, isValid, state }` so your backend has a single code path for > entitlement + refund detection. IAPKit is a single-package Bun + Hono server bundled with a Convex backend @@ -31,11 +31,13 @@ is an alias of `/v1/purchase/verify`. Pick `/v1/purchase/verify` for new code. - Horizon — `{ store: "horizon", userId, sku }` (≤ 256 chars each). IAPKit holds the App ID + App Secret server-side and composes the `OC|APP_ID|APP_SECRET` access token per-request. +- Amazon — `{ store: "amazon", userId, receiptId, sandbox? }` where + `userId` and `receiptId` come from Amazon Appstore RVS. ## Success response ```json -{ "isValid": true, "state": "ENTITLED" } +{ "store": "amazon", "isValid": true, "state": "ENTITLED" } ``` Harmonized `state` values (truthy `isValid`): `ENTITLED`, @@ -65,6 +67,7 @@ Harmonized `state` values (truthy `isValid`): `ENTITLED`, - [/docs/verification/apple](https://kit.openiap.dev/docs/verification/apple) — bundle ID, Issuer ID, Key ID, .p8 - [/docs/verification/google](https://kit.openiap.dev/docs/verification/google) — package name, service account JSON - [/docs/verification/horizon](https://kit.openiap.dev/docs/verification/horizon) — App ID + App Secret (write-only) +- [/docs/api](https://kit.openiap.dev/docs/api) — includes Amazon RVS userId + receiptId verification payloads - [/docs/api](https://kit.openiap.dev/docs/api) — request shapes, responses, errors, headers - [/docs/operations](https://kit.openiap.dev/docs/operations) — rate limits, logs, `/health`, graceful shutdown - [openiap.dev/docs/webhooks](https://openiap.dev/docs/webhooks) — operator setup steps for the lifecycle webhook URL (Apple ASN v2 + Google RTDN) and SDK code for consuming the SSE stream diff --git a/packages/kit/src/content/faq.md b/packages/kit/src/content/faq.md index 0bbafec7..d2d384f0 100644 --- a/packages/kit/src/content/faq.md +++ b/packages/kit/src/content/faq.md @@ -6,19 +6,19 @@ Run validation immediately after your app receives a purchase receipt and again ## Why should I perform receipt validation? -Receipts are the only authoritative source for whether a customer actually paid. Validating every transaction with a trusted server protects revenue by detecting refunded, duplicated, or jailbroken transactions before your app unlocks local access or your backend serves paid resources. It also gives you consistent purchase metadata for analytics, entitlement systems, and customer support because each validation returns normalized data across Apple and Google. +Receipts are the only authoritative source for whether a customer actually paid. Validating every transaction with a trusted server protects revenue by detecting refunded, duplicated, or jailbroken transactions before your app unlocks local access or your backend serves paid resources. It also gives you consistent purchase metadata for analytics, entitlement systems, and customer support because each validation returns normalized data across Apple, Google, Meta Horizon, and Amazon. ## What is receipt validation? -Receipt validation is the process of sending a store-issued purchase token (Apple receipt, Google purchase token, Play Billing signature, etc.) to a trusted server so it can verify the signature with Apple or Google, confirm product identifiers, amounts, and expiration, and return a definitive truth about the purchase. IAPKit abstracts the App Store and Play billing APIs behind a single REST endpoint and webhooks so your app can treat every purchase in the same way. +Receipt validation is the process of sending a store-issued purchase token (Apple receipt, Google purchase token, Horizon entitlement data, Amazon receipt ID, etc.) to a trusted server so it can verify the signature or entitlement with the source store, confirm product identifiers, amounts, and expiration, and return a definitive truth about the purchase. IAPKit abstracts supported store APIs behind a single REST endpoint and webhooks so your app can treat every purchase in the same way. ## Doesn't App Store or Google Play perform this securely already? -Apple and Google guarantee that receipts they issue are cryptographically signed, but validation still needs trusted server infrastructure instead of the device alone. Relying on local client state leaves you exposed to replayed receipts, tampered sandbox environments, and revoked subscriptions that the device hasn’t synced yet. Managed validation through IAPKit closes that gap by calling the official StoreKit and Google Play APIs, applying fraud heuristics, and giving you auditable logs if the stores ever dispute a transaction. +Store providers guarantee receipts or entitlement responses according to their own platform rules, but validation still needs trusted server infrastructure instead of the device alone. Relying on local client state leaves you exposed to replayed receipts, tampered sandbox environments, and revoked subscriptions that the device hasn’t synced yet. Managed validation through IAPKit closes that gap by calling the official store APIs, applying fraud heuristics, and giving you auditable logs if a store ever disputes a transaction. ## Will this prevent tools like Lucky Patcher? -Lucky Patcher-style tools only work when the purchase flow is trusted on-device. Because IAPKit never trusts local client state, every transaction is verified directly with Apple and Google before your app unlocks access or your backend grants an entitlement. A patched app can fake UI states, but it cannot forge the signed receipts that the stores return, so the validation step fails and the fraudulent purchase is rejected. Combine this with periodic revalidation or webhook-driven revocation to catch any attempts that slip through while the client is offline. +Lucky Patcher-style tools only work when the purchase flow is trusted on-device. Because IAPKit never trusts local client state, every transaction is verified directly with the source store before your app unlocks access or your backend grants an entitlement. A patched app can fake UI states, but it cannot forge store-issued receipts or entitlement responses, so the validation step fails and the fraudulent purchase is rejected. Combine this with periodic revalidation or webhook-driven revocation to catch any attempts that slip through while the client is offline. ## Do you track consumption state of consumable IAPs? diff --git a/packages/kit/src/content/terms-of-service.md b/packages/kit/src/content/terms-of-service.md index 5b2a4566..84e78673 100644 --- a/packages/kit/src/content/terms-of-service.md +++ b/packages/kit/src/content/terms-of-service.md @@ -142,7 +142,7 @@ We disclaim all warranties, express or implied, including: - Fitness for a particular purpose - Non-infringement -We do not guarantee accuracy of validation results beyond what is provided by Apple or Google. +We do not guarantee accuracy of validation results beyond what is provided by the source store. --- diff --git a/packages/kit/src/pages/blog/index.tsx b/packages/kit/src/pages/blog/index.tsx index 0da6c9b2..e2cf2b96 100644 --- a/packages/kit/src/pages/blog/index.tsx +++ b/packages/kit/src/pages/blog/index.tsx @@ -4,7 +4,7 @@ import { useSeoMeta, SITE_ORIGIN } from "@/hooks/useSeoMeta"; import { POSTS } from "./posts"; const BLOG_DESCRIPTION = - "Announcements, roadmap, and engineering notes from IAPKit — the OpenIAP receipt-validation service for App Store, Google Play, and Meta Horizon."; + "Announcements, roadmap, and engineering notes from IAPKit — the OpenIAP receipt-validation service for App Store, Google Play, Meta Horizon, and Amazon Appstore."; export default function BlogIndex() { const jsonLd = useMemo( diff --git a/packages/kit/src/pages/blog/posts.ts b/packages/kit/src/pages/blog/posts.ts index 825bb2fd..00a42a92 100644 --- a/packages/kit/src/pages/blog/posts.ts +++ b/packages/kit/src/pages/blog/posts.ts @@ -26,7 +26,7 @@ export const POSTS: BlogPost[] = [ excerpt: "We're moving IAPKit under OpenIAP and dropping every paywall. Receipt validation is the ground floor of IAP — it shouldn't be a tier you pay for.", description: - "IAPKit is joining OpenIAP and going completely free. No paywall, no plans, no usage limits on App Store, Google Play, and Meta Horizon receipt validation.", + "IAPKit is joining OpenIAP and going completely free. No paywall, no plans, no usage limits on App Store, Google Play, Meta Horizon, and Amazon Appstore receipt validation.", keywords: [ "IAPKit", "OpenIAP", @@ -35,6 +35,8 @@ export const POSTS: BlogPost[] = [ "App Store", "Google Play", "Meta Horizon", + "Amazon Appstore", + "Fire OS", "free IAP", ], author: BLOG_AUTHOR, diff --git a/packages/kit/src/pages/docs/sections/introduction.tsx b/packages/kit/src/pages/docs/sections/introduction.tsx index d34b30de..a37a97d4 100644 --- a/packages/kit/src/pages/docs/sections/introduction.tsx +++ b/packages/kit/src/pages/docs/sections/introduction.tsx @@ -1,5 +1,5 @@ import { Link } from "react-router-dom"; -import { Apple, Smartphone, Headset } from "lucide-react"; +import { Apple, Smartphone, Headset, ShoppingBag } from "lucide-react"; import { Callout } from "../components/Callout"; import { DocsPage } from "../components/DocsPage"; @@ -9,16 +9,16 @@ export default function IntroductionPage() {

    - IAPKit, managed by OpenIAP, is a hosted - receipt-validation service for mobile and VR apps that need server-side - store verification without building their own receipt server. Your app - sends a store-specific receipt to /v1/purchase/verify, - IAPKit calls the upstream store with credentials it already holds for - your project, and returns a normalized{" "} - {`{ isValid, state, productId }`} result your app can use. + IAPKit, managed by OpenIAP, is a receipt-validation + backend for mobile and VR apps that need server-side store verification + without building their own receipt server. You send a store-specific + receipt to /v1/purchase/verify, IAPKit calls the upstream + store with credentials it already holds for your project, and returns a + normalized {`{ store, isValid, state, productId? }`} result + your app can use.

    When to reach for IAPKit

    @@ -28,20 +28,20 @@ export default function IntroductionPage() { user still has an entitlement — a refund, a chargeback, a revoked subscription, or a replayed receipt on a jailbroken device all look identical to a fresh purchase from the client's perspective. - Validating server-to-server against Apple / Google / Meta is the only - way to be certain, and that validation needs store credentials that must - stay inside IAPKit, not on a customer device. + Validating server-to-server against Apple / Google / Horizon / Amazon is + the only way to be certain, and that validation needs store credentials + that must stay inside IAPKit, not on a customer device.

    - IAPKit centralizes those credentials in one place, exposes one managed - API your app can call directly with a project API key, and harmonizes - the three stores' very different response shapes into a single - lifecycle: ENTITLED, PENDING_ACKNOWLEDGMENT,{" "} + IAPKit centralizes those credentials in one place, exposes one API your + app calls with a project API key, and harmonizes the supported stores' + very different response shapes into a single lifecycle:{" "} + ENTITLED, PENDING_ACKNOWLEDGMENT,{" "} CANCELED, and friends.

    Supported stores

    -
    +
    } title="Apple App Store" @@ -60,6 +60,12 @@ export default function IntroductionPage() { detail="Quest entitlements verified via Meta Graph API. App Secret stays server-side; clients send only (userId, sku)." slug="verification/horizon" /> + } + title="Amazon Appstore" + detail="Fire OS receipts verified through Amazon RVS using the project's shared secret. Clients send only (userId, receiptId)." + slug="api" + />

    Architecture

    @@ -75,10 +81,10 @@ export default function IntroductionPage() { ───────────────────── ────────────────── ───────────────── POST /v1/purchase/verify apiKey → project Bearer ───► verify action ───► App Store / Play / - { store, ... } Meta Graph API + { store, ... } Horizon / Amazon ◄── verified receipt - { isValid, state, - productId } ◄─── harmonized state + { store, isValid, state, + productId? } ◄─── harmonized state `} diff --git a/packages/kit/src/pages/docs/sections/projects.tsx b/packages/kit/src/pages/docs/sections/projects.tsx index 5b75c534..642c419e 100644 --- a/packages/kit/src/pages/docs/sections/projects.tsx +++ b/packages/kit/src/pages/docs/sections/projects.tsx @@ -23,7 +23,7 @@ export default function ProjectsPage() {
             {`Organization (members, usage)
       └── Project (one mobile app)
    -        ├── Store credentials (Apple, Google, Horizon)
    +        ├── Store credentials (Apple, Google, Horizon, Amazon)
             └── API keys ── used by your app
     `}
           
    diff --git a/packages/kit/src/pages/docs/sections/verification-apple.tsx b/packages/kit/src/pages/docs/sections/verification-apple.tsx index 46339351..f96ea5b3 100644 --- a/packages/kit/src/pages/docs/sections/verification-apple.tsx +++ b/packages/kit/src/pages/docs/sections/verification-apple.tsx @@ -177,6 +177,7 @@ if (revocationDate !== undefined) { language="json" > {`{ + "store": "apple", "isValid": false, "state": "CANCELED", "productId": "premium_monthly" diff --git a/packages/kit/src/pages/landing.tsx b/packages/kit/src/pages/landing.tsx index 935d3464..617e40ed 100644 --- a/packages/kit/src/pages/landing.tsx +++ b/packages/kit/src/pages/landing.tsx @@ -121,7 +121,7 @@ export default function LandingPage() { {"Purpose-built for fraud-resistant IAP validation"}

    - {"One API to confirm every App Store and Play Store purchase."} + {"One API to confirm every supported store purchase."}

    @@ -149,7 +149,7 @@ export default function LandingPage() {

    { - "Add one IAPKit call and handle both stores with the same payload. You'll be up and running in minutes." + "Add one IAPKit call and handle every supported store with the same payload. You'll be up and running in minutes." }

    @@ -181,7 +181,7 @@ export default function LandingPage() {

    { - "Collect the receipt on-device, send it to IAPKit, and unlock the item once we confirm it with Apple or Google." + "Collect the receipt on-device, send it to IAPKit, and unlock the item once the source store confirms it." }

    @@ -177,10 +183,11 @@ function FireOSSetup() {

    - React Native and Expo config plugins write the Fire OS Gradle - selection during prebuild. Flutter apps should wire the same property - into the app module's missingDimensionStrategy. Enable - Fire OS builds in the app's android/gradle.properties: + Expo and React Native config plugins use amazon.fireOS. + Flutter builds, or React Native builds that do not run a config + plugin, can select the same Android flavor through the{' '} + fireOsEnabled Gradle property and the app module's{' '} + missingDimensionStrategy:

    {`fireOsEnabled=true # Do not set horizonEnabled=true in the same build.`} @@ -189,10 +196,10 @@ function FireOSSetup() { selection during prebuild:

    {`// Expo -plugins: [['expo-iap', { modules: { fireOS: true } }]] +plugins: [['expo-iap', { amazon: { fireOS: true } }]] // React Native config plugin -plugins: [['react-native-iap', { modules: { fireOS: true } }]]`} +plugins: [['react-native-iap', { amazon: { fireOS: true } }]]`}

    For Flutter, read the property from{' '} android/gradle.properties in{' '} @@ -267,7 +274,8 @@ await requestPurchase({ restorePurchases {' '} - use PurchasingService.getPurchaseUpdates(reset=true). + stay on the OpenIAP surface while the Amazon adapter internally + reads purchase updates from the Amazon Appstore SDK.

  • @@ -286,6 +294,37 @@ await requestPurchase({ +
    +

    + IAPKit Verification + + # + +

    +

    + Fire OS purchases can use IAPKit's Amazon verification path instead of + app-owned receipt-verification infrastructure. Pass the Amazon receipt + id through iapkit.amazon.receiptId; the Amazon Android + flavor can resolve userId from Amazon user data when it + is omitted. Use sandbox: true for Amazon App Tester + receipts. +

    + {`const result = await verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + apiKey: '', + amazon: { + receiptId: purchase.purchaseToken ?? '', + sandbox: __DEV__, + }, + }, +});`} +

    + See the IAPKit backend guide for + the shared Fire OS and Vega OS Amazon verification flow. +

    +
    +

    Current Limitations @@ -296,12 +335,16 @@ await requestPurchase({
    • Server-side Amazon Receipt Verification Service integration is not - included in the Android client package. + embedded in the Android client package. Use IAPKit's Amazon + verification path, or run your own server-side RVS integration.
    • - Sandbox testing still requires Amazon App Tester setup and Amazon's - sandbox mode, for example{' '} - adb shell setprop debug.amazon.sandboxmode debug. + Fire OS sandbox testing still requires Amazon App Tester setup and + the Android sandbox property, for example{' '} + adb shell setprop debug.amazon.sandboxmode debug. Vega + OS sandbox testing uses the app-local{' '} + amazon.config.json flow documented in the{' '} + Vega OS Runtime guide.
    • Vega OS is intentionally not included in the Fire OS Android flavor. diff --git a/packages/docs/src/pages/docs/kit-backend.tsx b/packages/docs/src/pages/docs/kit-backend.tsx index a98484e5..c9fad9b5 100644 --- a/packages/docs/src/pages/docs/kit-backend.tsx +++ b/packages/docs/src/pages/docs/kit-backend.tsx @@ -15,7 +15,7 @@ function KitBackend() { title="Purchase Verification with IAPKit" description="Purchase verification with IAPKit at kit.openiap.dev handles Apple StoreKit 2, Google Play, Amazon Appstore, and Meta Horizon verification, lifecycle webhooks, subscription state, revenue metrics, and store product sync — without the host app needing to operate its own server." path="/docs/kit-backend" - keywords="IAPKit, kit.openiap.dev, OpenIAP kit, hosted backend, purchase verification, receipt validation, subscription state, App Store Connect, Play Console, MCP server" + keywords="IAPKit, kit.openiap.dev, OpenIAP kit, hosted backend, purchase verification, receipt validation, Amazon Fire OS, Vega OS, subscription state, App Store Connect, Play Console, MCP server" />

      Purchase Verification

      @@ -31,6 +31,13 @@ function KitBackend() { is exposed through one URL surface that the framework SDKs and MCP server speak.

      +

      + Amazon targets use the same IAPKit Amazon verification shape. Fire OS + purchases from the Android amazon flavor and Vega OS + purchases from the Kepler runtime both resolve an Amazon user id and + receipt id through iapkit.amazon, then IAPKit verifies them + through Amazon RVS and stores the result under amazon. +

      @@ -46,8 +53,8 @@ function KitBackend() {
      • POST /v1/purchase/verify — purchase verification (Apple - JWS, Google purchaseToken, Amazon RVS receiptId, Meta Horizon) with - a Bearer API key. + JWS, Google purchaseToken, Amazon RVS receiptId for Fire OS and Vega + OS, Meta Horizon) with a Bearer API key.
      • POST /v1/webhooks/{apiKey} — unified App @@ -170,14 +177,23 @@ function KitBackend() { verifyPurchaseWithProvider in the SDKs; the snippets below call it directly.

        +

        + For Fire OS and Vega OS, choose the Amazon branch and pass the Amazon + receipt ID. The SDK resolves the Amazon user ID from the runtime when + available. Set sandbox: true when validating Amazon App + Tester sandbox receipts. +

        {{ typescript: ( {`import { Platform } from 'react-native'; -import { verifyPurchaseWithProvider, type Purchase } from 'expo-iap'; +import { verifyPurchaseWithProvider } from 'expo-iap'; // Same API in react-native-iap. const token = purchase.purchaseToken ?? ''; +const runtimeOS = Platform.OS as string; +const isFireOSBuild = process.env.EXPO_PUBLIC_STORE === 'amazon'; +const isAmazonRuntime = runtimeOS === 'kepler' || isFireOSBuild; const result = await verifyPurchaseWithProvider({ provider: 'iapkit', iapkit: { @@ -185,8 +201,14 @@ const result = await verifyPurchaseWithProvider({ apiKey: process.env.EXPO_PUBLIC_IAPKIT_API_KEY, ...(Platform.OS === 'ios' ? { apple: { jws: token } } + : isAmazonRuntime + ? { + amazon: { + receiptId: token, + sandbox: __DEV__, + }, + } : { google: { purchaseToken: token } }), - // Fire OS: pass amazon: { userId, receiptId } instead of google. }, }); @@ -225,6 +247,7 @@ val result = module.verifyPurchaseWithProvider( purchaseToken = purchase.purchaseToken.orEmpty(), ), // Fire OS: use amazon = RequestVerifyPurchaseWithIapkitAmazonProps(...) + // with userId, receiptId, and sandbox for Amazon App Tester. ), ), ) @@ -252,6 +275,7 @@ final result = await FlutterInappPurchase.instance.verifyPurchaseWithProvider( purchaseToken: purchase.purchaseToken ?? '', ) : null, + // Fire OS builds can pass amazon with userId, receiptId, and sandbox. ), ), ); @@ -275,6 +299,7 @@ var result = await mutate.VerifyPurchaseWithProviderAsync( ApiKey = iapkitApiKey, Apple = new RequestVerifyPurchaseWithIapkitAppleProps { Jws = token }, Google = new RequestVerifyPurchaseWithIapkitGoogleProps { PurchaseToken = token }, + // Amazon Fire OS uses Amazon = new RequestVerifyPurchaseWithIapkitAmazonProps { ... }. }, }); @@ -294,6 +319,7 @@ val result = kmpIAP.verifyPurchaseWithProvider( apiKey = iapkitApiKey, apple = if (isIos) RequestVerifyPurchaseWithIapkitAppleProps(jws = token) else null, google = if (!isIos) RequestVerifyPurchaseWithIapkitGoogleProps(purchaseToken = token) else null, + // Amazon Fire OS builds use amazon with userId, receiptId, and sandbox. ), ), ) diff --git a/packages/docs/src/pages/docs/setup/expo.tsx b/packages/docs/src/pages/docs/setup/expo.tsx index 59a59aed..4f33d02f 100644 --- a/packages/docs/src/pages/docs/setup/expo.tsx +++ b/packages/docs/src/pages/docs/setup/expo.tsx @@ -286,9 +286,11 @@ cd ios && pod install`} "iapkitApiKey": "openiap-kit_", "modules": { "onside": true, - "horizon": true, + "horizon": true + }, + "amazon": { "fireOS": false, - "vega": false + "vegaOS": false }, "android": { "horizonAppId": "YOUR_HORIZON_APP_ID" @@ -299,6 +301,13 @@ cd ios && pod install`} } }`} +

        + Amazon targets are grouped under amazon.{' '} + amazon.fireOS selects the Android Amazon Appstore flavor, + while amazon.vegaOS prepares Kepler/Vega project files. + They can both be true in one config, but Fire OS and Vega + OS are still built as separate artifacts. +

        @@ -340,7 +349,7 @@ cd ios && pod install`} + + + + +
        - modules.fireOS + amazon.fireOS boolean @@ -350,7 +359,7 @@ cd ios && pod install`}
        - modules.vega + amazon.vegaOS boolean diff --git a/packages/docs/src/pages/docs/setup/react-native.tsx b/packages/docs/src/pages/docs/setup/react-native.tsx index a44ca4b1..c3840177 100644 --- a/packages/docs/src/pages/docs/setup/react-native.tsx +++ b/packages/docs/src/pages/docs/setup/react-native.tsx @@ -192,13 +192,17 @@ end`} automatic service reconnection
      • - For Fire OS builds, set fireOsEnabled=true in{' '} - android/gradle.properties and see the{' '} - Fire OS Setup Guide + For Fire OS builds, use amazon.fireOS=true with the{' '} + react-native-iap config plugin, or set{' '} + fireOsEnabled=true in{' '} + android/gradle.properties when configuring Gradle + directly. See the{' '} + Fire OS Setup Guide.
      • - For Vega OS, do not use an Android flavor. Install Amazon's Vega IAP - package and follow the{' '} + For Vega OS, do not use an Android flavor. Use{' '} + amazon.vegaOS=true as the config plugin target marker, + install Amazon's Vega IAP package, and follow the{' '} Vega OS Runtime guide.
      • diff --git a/packages/docs/src/pages/docs/types/verify-purchase-with-provider-props.tsx b/packages/docs/src/pages/docs/types/verify-purchase-with-provider-props.tsx index 1312671c..9abec37b 100644 --- a/packages/docs/src/pages/docs/types/verify-purchase-with-provider-props.tsx +++ b/packages/docs/src/pages/docs/types/verify-purchase-with-provider-props.tsx @@ -130,6 +130,18 @@ function VerifyPurchaseWithProviderProps() {
        Google/Android verification parameters.
        + amazon + + RequestVerifyPurchaseWithIapkitAmazonProps? + + Amazon Appstore verification parameters for Fire OS and Vega OS + purchase receipts. +
        @@ -189,6 +201,64 @@ function VerifyPurchaseWithProviderProps() { + + + RequestVerifyPurchaseWithIapkitAmazonProps + +

        + Amazon Appstore receipt verification parameters. Fire OS and Vega OS + both use this amazon payload when verifying through + IAPKit. +

        + + + + + + + + + + + + + + + + + + + + + + + + + +
        NameTypeSummary
        + userId + + string? + + Amazon Appstore user id returned by{' '} + PurchaseResponse.getUserData().getUserId(). +
        + receiptId + + string + + Amazon Appstore receipt id returned by{' '} + PurchaseResponse.getReceipt().getReceiptId(). +
        + sandbox + + boolean? + + Use Amazon RVS Cloud Sandbox for Amazon App Tester receipts. +
      ); diff --git a/packages/docs/src/pages/docs/updates/announcements.tsx b/packages/docs/src/pages/docs/updates/announcements.tsx index b0b00fc8..37174703 100644 --- a/packages/docs/src/pages/docs/updates/announcements.tsx +++ b/packages/docs/src/pages/docs/updates/announcements.tsx @@ -90,7 +90,10 @@ function Announcements() { expo-iap config-plugin support for compatible Expo projects. Together, they help developers keep one familiar OpenIAP purchase flow while reaching Amazon-specific purchase environments. - The implementation is tracked in{' '} + IAPKit uses the same Amazon verification path for Fire OS and Vega + OS receipts, so apps can keep managed receipt checks and entitlement + state in the same OpenIAP backend flow. The implementation is + tracked in{' '} +
    • + IAPKit verification: Fire OS and Vega OS + purchases can verify through IAPKit's Amazon receipt path with the + same iapkit.amazon payload. +
    • Catalog identity: Product IDs stay aligned across Amazon Appstore, Amazon App Tester, app code, and Kit entitlement @@ -166,7 +174,11 @@ function Announcements() { Vega OS runtime guide {' '} - for React Native for Vega apps and compatible Expo projects. + for React Native for Vega apps and compatible Expo projects. Use the{' '} + + IAPKit backend guide + {' '} + for Amazon receipt verification and entitlement checks. ), diff --git a/packages/kit/Dockerfile b/packages/kit/Dockerfile index 536f90e8..fe874324 100644 --- a/packages/kit/Dockerfile +++ b/packages/kit/Dockerfile @@ -75,6 +75,8 @@ COPY --from=builder --chown=openiap:openiap /app/packages/kit/dist ./public ENV STATIC_ROOT=/srv/public ENV PORT=3000 +ENV NODE_ENV=production +ENV APP_ENV=production EXPOSE 3000/tcp USER openiap diff --git a/packages/kit/server/api/v1/request-logger.test.ts b/packages/kit/server/api/v1/request-logger.test.ts index 5e3a9c43..6d9e2215 100644 --- a/packages/kit/server/api/v1/request-logger.test.ts +++ b/packages/kit/server/api/v1/request-logger.test.ts @@ -4,6 +4,7 @@ import { Hono } from "hono"; import { apiKeyMiddleware } from "./middleware"; import { requestLoggerMiddleware, + type VerifyDebugLogLine, type VerifyLogLine, type VerifyOutcome, } from "./request-logger"; @@ -31,6 +32,8 @@ type TestVars = { function buildApp(params: { logs: VerifyLogLine[]; + debugLogs?: VerifyDebugLogLine[]; + debug?: boolean; now?: () => number; handler?: (c: { set: (k: "verifyOutcome", v: VerifyOutcome) => void; @@ -48,6 +51,8 @@ function buildApp(params: { apiKeyMiddleware, requestLoggerMiddleware({ logger: (line) => params.logs.push(line), + debugLogger: (line) => params.debugLogs?.push(line), + debug: params.debug, now: params.now ?? defaultNow, newCorrId: () => "corr-fixed", }), @@ -196,6 +201,114 @@ describe("requestLoggerMiddleware", () => { expect(logs[0].store).toBe("amazon"); }); + test("emits redacted non-production debug logs when enabled", async () => { + const logs: VerifyLogLine[] = []; + const debugLogs: VerifyDebugLogLine[] = []; + const app = buildApp({ logs, debugLogs, debug: true }); + + const res = await app.request("/verify", { + method: "POST", + headers: { + Authorization: "Bearer key-amazon", + "content-type": "application/json", + }, + body: JSON.stringify({ + store: "amazon", + userId: TEST_AMAZON_USER_ID, + receiptId: TEST_AMAZON_RECEIPT_ID, + sandbox: true, + }), + }); + + expect(res.status).toBe(200); + expect(logs).toHaveLength(1); + expect(debugLogs).toHaveLength(1); + + const line = debugLogs[0]; + expect(line.kind).toBe("verify_request_debug"); + expect(line.corrId).toBe("corr-fixed"); + expect(line.statusCode).toBe(200); + expect(line.store).toBe("amazon"); + expect(line.sandbox).toBe(true); + expect(line.apiKeyHash).toMatch(/^[0-9a-f]{16}$/); + expect(line.identifiers?.userId).toEqual({ + length: TEST_AMAZON_USER_ID.length, + sha256Prefix: expect.stringMatching(/^[0-9a-f]{16}$/), + }); + expect(line.identifiers?.receiptId).toEqual({ + length: TEST_AMAZON_RECEIPT_ID.length, + sha256Prefix: expect.stringMatching(/^[0-9a-f]{16}$/), + }); + + const serializedLine = JSON.stringify(line); + expect(serializedLine).not.toContain("key-amazon"); + expect(serializedLine).not.toContain(TEST_AMAZON_USER_ID); + expect(serializedLine).not.toContain(TEST_AMAZON_RECEIPT_ID); + }); + + test("does not emit debug logs when disabled", async () => { + const logs: VerifyLogLine[] = []; + const debugLogs: VerifyDebugLogLine[] = []; + const app = buildApp({ logs, debugLogs, debug: false }); + + const res = await app.request("/verify", { + method: "POST", + headers: { + Authorization: "Bearer key-amazon", + "content-type": "application/json", + }, + body: JSON.stringify({ + store: "amazon", + userId: TEST_AMAZON_USER_ID, + receiptId: TEST_AMAZON_RECEIPT_ID, + sandbox: true, + }), + }); + + expect(res.status).toBe(200); + expect(logs).toHaveLength(1); + expect(debugLogs).toHaveLength(0); + }); + + test("keeps debug logs off in production even when explicitly requested", async () => { + const previousNodeEnv = process.env.NODE_ENV; + const previousKitDebug = process.env.KIT_DEBUG_VERIFY_LOGS; + + process.env.NODE_ENV = "production"; + process.env.KIT_DEBUG_VERIFY_LOGS = "1"; + + try { + const logs: VerifyLogLine[] = []; + const debugLogs: VerifyDebugLogLine[] = []; + const app = buildApp({ logs, debugLogs, debug: true }); + + const res = await app.request("/verify", { + method: "POST", + headers: { + Authorization: "Bearer key-amazon", + "content-type": "application/json", + }, + body: JSON.stringify({ + store: "amazon", + userId: TEST_AMAZON_USER_ID, + receiptId: TEST_AMAZON_RECEIPT_ID, + sandbox: true, + }), + }); + + expect(res.status).toBe(200); + expect(logs).toHaveLength(1); + expect(debugLogs).toHaveLength(0); + } finally { + process.env.NODE_ENV = previousNodeEnv; + if (previousKitDebug === undefined) { + delete process.env.KIT_DEBUG_VERIFY_LOGS; + } else { + process.env.KIT_DEBUG_VERIFY_LOGS = previousKitDebug; + } + } + }); + test("populates the X-Correlation-Id response header even on validator failure", async () => { const logs: VerifyLogLine[] = []; const app = buildApp({ logs }); diff --git a/packages/kit/server/api/v1/request-logger.ts b/packages/kit/server/api/v1/request-logger.ts index 19539694..6b21a39c 100644 --- a/packages/kit/server/api/v1/request-logger.ts +++ b/packages/kit/server/api/v1/request-logger.ts @@ -29,7 +29,40 @@ export interface VerifyLogLine { state?: string; } +export interface RedactedDebugValue { + length: number; + sha256Prefix: string; +} + +export interface VerifyDebugIdentifiers { + jws?: RedactedDebugValue; + purchaseToken?: RedactedDebugValue; + receiptId?: RedactedDebugValue; + userId?: RedactedDebugValue; + expectedProductId?: string; + sku?: string; +} + +export interface VerifyDebugLogLine { + kind: "verify_request_debug"; + corrId: string; + method: string; + path: string; + statusCode: number; + durationMs: number; + apiKeyHash?: string; + store?: VerifyStore; + isValid?: boolean; + state?: string; + sandbox?: boolean; + identifiers?: VerifyDebugIdentifiers; +} + export type VerifyLogger = (line: VerifyLogLine) => void; +export type VerifyDebugLogger = (line: VerifyDebugLogLine) => void; +type ValidatedJsonReader = ( + target: "json", +) => VerifyRequestBodyForLog | undefined; export const defaultVerifyLogger: VerifyLogger = (line) => { // One JSON line, `kind` up front so log queries can filter cheaply. @@ -37,12 +70,117 @@ export const defaultVerifyLogger: VerifyLogger = (line) => { console.log(JSON.stringify({ level: "info", ...line })); }; +export const defaultVerifyDebugLogger: VerifyDebugLogger = (line) => { + console.log(JSON.stringify({ level: "debug", ...line })); +}; + function describeErrorForLog(error: unknown): string { return error instanceof Error ? error.name : typeof error; } +function isProductionRuntime(): boolean { + return ( + process.env.NODE_ENV === "production" || + process.env.APP_ENV === "production" || + process.env.FLY_APP_NAME === "openiap-kit" + ); +} + +function shouldEnableDefaultDebugLogging(): boolean { + if (isProductionRuntime()) { + return false; + } + + if (process.env.KIT_DEBUG_VERIFY_LOGS === "1") { + return true; + } + + if (process.env.KIT_DEBUG_VERIFY_LOGS === "0") { + return false; + } + + if (process.env.NODE_ENV === "test" || process.env.VITEST === "true") { + return false; + } + + return true; +} + +function redactDebugValue(value: unknown): RedactedDebugValue | undefined { + if (typeof value !== "string" || value.length === 0) { + return undefined; + } + + return { + length: value.length, + sha256Prefix: crypto + .createHash("sha256") + .update(value) + .digest("hex") + .slice(0, 16), + }; +} + +function includePlainDebugValue(value: unknown): string | undefined { + if (typeof value !== "string" || value.length === 0) { + return undefined; + } + + return value; +} + +interface VerifyRequestBodyForLog { + store?: VerifyStore; + jws?: string; + purchaseToken?: string; + receiptId?: string; + userId?: string; + expectedProductId?: string; + sku?: string; + sandbox?: boolean; +} + +function collectDebugIdentifiers( + body: VerifyRequestBodyForLog | undefined, +): VerifyDebugIdentifiers | undefined { + if (!body) { + return undefined; + } + + const identifiers: VerifyDebugIdentifiers = {}; + const jws = redactDebugValue(body.jws); + const purchaseToken = redactDebugValue(body.purchaseToken); + const receiptId = redactDebugValue(body.receiptId); + const userId = redactDebugValue(body.userId); + const expectedProductId = includePlainDebugValue(body.expectedProductId); + const sku = includePlainDebugValue(body.sku); + + if (jws) { + identifiers.jws = jws; + } + if (purchaseToken) { + identifiers.purchaseToken = purchaseToken; + } + if (receiptId) { + identifiers.receiptId = receiptId; + } + if (userId) { + identifiers.userId = userId; + } + if (expectedProductId) { + identifiers.expectedProductId = expectedProductId; + } + if (sku) { + identifiers.sku = sku; + } + + return Object.keys(identifiers).length > 0 ? identifiers : undefined; +} + export interface RequestLoggerConfig { logger?: VerifyLogger; + debugLogger?: VerifyDebugLogger; + debug?: boolean; now?: () => number; newCorrId?: () => string; } @@ -61,6 +199,10 @@ export function requestLoggerMiddleware( config: RequestLoggerConfig = {}, ): ReturnType> { const log = config.logger ?? defaultVerifyLogger; + const debugLog = config.debugLogger ?? defaultVerifyDebugLogger; + const shouldLogDebug = + !isProductionRuntime() && + (config.debug ?? shouldEnableDefaultDebugLogging()); const clock = config.now ?? (() => Date.now()); const newCorrId = config.newCorrId ?? (() => crypto.randomUUID()); @@ -84,10 +226,12 @@ export function requestLoggerMiddleware( const durationMs = clock() - start; let store: VerifyStore | undefined; + let body: VerifyRequestBodyForLog | undefined; try { - const body = c.req.valid("json" as never) as - | { store?: VerifyStore } - | undefined; + const readValidatedJson = c.req.valid.bind( + c.req, + ) as ValidatedJsonReader; + body = readValidatedJson("json"); store = body?.store; } catch { // Validator rejected or never ran — body may be malformed. @@ -101,6 +245,7 @@ export function requestLoggerMiddleware( // tests that don't mount the rate limiter). const apiKeyHash = c.var.apiKeyHash ?? (apiKey ? hashApiKey(apiKey) : undefined); + const statusCode = nextError && c.res.status < 400 ? 500 : c.res.status; // Swallow logger-side throws — a broken sink should never take // down a request whose real work already succeeded (or already @@ -112,7 +257,7 @@ export function requestLoggerMiddleware( corrId, method: c.req.method, path: c.req.path, - statusCode: nextError && c.res.status < 400 ? 500 : c.res.status, + statusCode, durationMs, apiKeyHash, store, @@ -125,6 +270,30 @@ export function requestLoggerMiddleware( describeErrorForLog(loggerError), ); } + + if (shouldLogDebug) { + try { + debugLog({ + kind: "verify_request_debug", + corrId, + method: c.req.method, + path: c.req.path, + statusCode, + durationMs, + apiKeyHash, + store, + isValid: outcome?.isValid, + state: outcome?.state, + sandbox: body?.sandbox, + identifiers: collectDebugIdentifiers(body), + }); + } catch (loggerError) { + console.error( + "request-debug-logger failed:", + describeErrorForLog(loggerError), + ); + } + } } }); } diff --git a/scripts/agent/compile-context.ts b/scripts/agent/compile-context.ts index 2f33ec0d..17aa2929 100644 --- a/scripts/agent/compile-context.ts +++ b/scripts/agent/compile-context.ts @@ -280,7 +280,8 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. - Google Play: default Android artifact, \`openiap-google\`. - Meta Horizon: Android \`horizon\` flavor, \`openiap-google-horizon\`. - Fire OS: Android \`amazon\` flavor, - \`openiap-google-amazon\`; set \`fireOsEnabled=true\` or + \`openiap-google-amazon\`; set \`amazon.fireOS=true\`, + \`fireOsEnabled=true\`, or \`missingDimensionStrategy("platform", "amazon")\`. Runtime adapters are wired for native Android, \`react-native-iap\`, \`expo-iap\`, and \`flutter_inapp_purchase\`; Godot, KMP, and MAUI have schema @@ -288,9 +289,10 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. - Vega OS: not an Android flavor. Target React Native for Vega / Expo only, using Amazon's JavaScript IAP API through the runtime-selected \`kepler\` adapter at the same runtime integration layer as Onside. In Expo or React - Native config plugin options, \`modules.vega=true\` is only a - runtime-support guard; it does not select an Android flavor and cannot be - combined with \`modules.fireOS\` or \`modules.horizon\`. + Native config plugin options, use \`amazon.vegaOS=true\`. It marks the Vega + runtime target and does not select an Android flavor. \`amazon.fireOS\` and + \`amazon.vegaOS\` can both be enabled when an app produces separate Fire OS + and Vega OS artifacts. ### Fire OS @@ -309,9 +311,10 @@ Fire OS maps OpenIAP calls to the Amazon Appstore SDK: ### Vega OS Runtime -Vega OS is not Fire OS and must not set \`fireOsEnabled=true\`; that flag is -only for Android Fire OS builds. Install -\`@amazon-devices/keplerscript-appstore-iap-lib\` and let +Vega OS is not Fire OS and is not selected with \`fireOsEnabled=true\`; that +flag is only for Android Fire OS builds. Use \`amazon.vegaOS=true\` for the +Vega runtime target and \`amazon.fireOS=true\` for separate Fire OS Android +artifacts. Install \`@amazon-devices/keplerscript-appstore-iap-lib\` and let \`react-native-iap\` / \`expo-iap\` select the \`kepler\` adapter at runtime, similar to how Onside is selected at the runtime integration layer. From 941957a98b7b2712571133cd44ffc570067be27f Mon Sep 17 00:00:00 2001 From: hyochan Date: Thu, 11 Jun 2026 02:31:32 +0900 Subject: [PATCH 45/51] fix(amazon): address review feedback --- libraries/expo-iap/plugin/src/withIAP.ts | 6 ++---- libraries/expo-iap/src/useIAP.ts | 4 ++-- .../main/java/com/margelo/nitro/iap/HybridRnIap.kt | 13 +++++++------ libraries/react-native-iap/plugin/src/withIAP.ts | 6 +----- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/libraries/expo-iap/plugin/src/withIAP.ts b/libraries/expo-iap/plugin/src/withIAP.ts index 87d8cf6c..70d28c7c 100644 --- a/libraries/expo-iap/plugin/src/withIAP.ts +++ b/libraries/expo-iap/plugin/src/withIAP.ts @@ -64,7 +64,7 @@ const HORIZON_APP_ID_META_DATA_NAME = type AndroidManifestLike = { manifest: { - application?: Array>; + application?: Record[]; }; }; @@ -137,10 +137,8 @@ export const modifyAppBuildGradle = ( isFireOsEnabled?: boolean, ): string => { function loadOpenIapAndroidVersion(): string { - const versionsPath = path.resolve(__dirname, '../../openiap-versions.json'); try { - const raw = fs.readFileSync(versionsPath, 'utf8'); - const parsed = JSON.parse(raw); + const parsed = require('../../openiap-versions.json'); const googleVersion = typeof parsed?.google === 'string' ? parsed.google.trim() : ''; if (!googleVersion) { diff --git a/libraries/expo-iap/src/useIAP.ts b/libraries/expo-iap/src/useIAP.ts index 4f05ded3..64ffdd90 100644 --- a/libraries/expo-iap/src/useIAP.ts +++ b/libraries/expo-iap/src/useIAP.ts @@ -696,8 +696,8 @@ export function useIAP(options?: UseIAPOptions): UseIap { } const friendly = getUserFriendlyErrorMessage(error); if ( - error.code !== ErrorCode.AlreadyOwned && - error.code !== ErrorCode.ServiceTimeout && + error?.code !== ErrorCode.AlreadyOwned && + error?.code !== ErrorCode.ServiceTimeout && !isUserCancelledError(error) && !isRecoverableError(error) ) { diff --git a/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt b/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt index 0be4fb65..ae5e7955 100644 --- a/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +++ b/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt @@ -43,6 +43,7 @@ import dev.hyo.openiap.ExternalLinkLaunchModeAndroid as OpenIapExternalLinkLaunc import dev.hyo.openiap.ExternalLinkTypeAndroid as OpenIapExternalLinkType import dev.hyo.openiap.listener.OpenIapDeveloperProvidedBillingListener import dev.hyo.openiap.store.OpenIapStore +import java.util.Locale import kotlin.coroutines.cancellation.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -1274,12 +1275,12 @@ class HybridRnIap : HybridRnIapSpec() { } private fun mapIapStore(store: dev.hyo.openiap.IapStore): IapStore { - return when (store.rawValue.lowercase(java.util.Locale.ROOT)) { - "apple" -> IapStore.APPLE - "google" -> IapStore.GOOGLE - "horizon" -> IapStore.HORIZON - "amazon" -> IapStore.AMAZON - else -> IapStore.UNKNOWN + return when (store) { + dev.hyo.openiap.IapStore.Apple -> IapStore.APPLE + dev.hyo.openiap.IapStore.Google -> IapStore.GOOGLE + dev.hyo.openiap.IapStore.Horizon -> IapStore.HORIZON + dev.hyo.openiap.IapStore.Amazon -> IapStore.AMAZON + dev.hyo.openiap.IapStore.Unknown -> IapStore.UNKNOWN } } diff --git a/libraries/react-native-iap/plugin/src/withIAP.ts b/libraries/react-native-iap/plugin/src/withIAP.ts index 7f2e4a94..b07c5431 100644 --- a/libraries/react-native-iap/plugin/src/withIAP.ts +++ b/libraries/react-native-iap/plugin/src/withIAP.ts @@ -10,8 +10,6 @@ import { } from 'expo/config-plugins'; import type {ConfigPlugin} from 'expo/config-plugins'; import type {ExpoConfig} from '@expo/config-types'; -import {readFileSync} from 'node:fs'; -import {resolve as resolvePath} from 'node:path'; const pkg = require('../../package.json'); @@ -62,10 +60,8 @@ const modifyAppBuildGradle = ( }; function loadOpenIapVersion(): string { - const versionsPath = resolvePath(__dirname, '../../openiap-versions.json'); try { - const raw = readFileSync(versionsPath, 'utf8'); - const parsed = JSON.parse(raw); + const parsed = require('../../openiap-versions.json'); const googleVersion = typeof parsed?.google === 'string' ? parsed.google.trim() : ''; if (!googleVersion) { From 7e5e99f2309470ba288b90a9888edcc1366cdceb Mon Sep 17 00:00:00 2001 From: hyochan Date: Thu, 11 Jun 2026 02:42:33 +0900 Subject: [PATCH 46/51] fix(vega): harden null response handling --- libraries/expo-iap/src/index.kepler.ts | 4 ++-- libraries/expo-iap/src/vega-adapter.ts | 3 ++- libraries/react-native-iap/src/vega-adapter.ts | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/libraries/expo-iap/src/index.kepler.ts b/libraries/expo-iap/src/index.kepler.ts index 65d96f2a..10832da0 100644 --- a/libraries/expo-iap/src/index.kepler.ts +++ b/libraries/expo-iap/src/index.kepler.ts @@ -65,8 +65,8 @@ const normalizePurchaseArray = (purchases: Purchase[]): Purchase[] => }); const getAndroidRequest = ( - request: RequestPurchasePropsByPlatforms | RequestSubscriptionPropsByPlatforms, -) => request.google ?? request.android; + request?: RequestPurchasePropsByPlatforms | RequestSubscriptionPropsByPlatforms | null, +) => request?.google ?? request?.android; const createPurchaseTokenError = (purchase: Purchase): Error => { const error = new Error( diff --git a/libraries/expo-iap/src/vega-adapter.ts b/libraries/expo-iap/src/vega-adapter.ts index 759da5c2..4c0645c6 100644 --- a/libraries/expo-iap/src/vega-adapter.ts +++ b/libraries/expo-iap/src/vega-adapter.ts @@ -685,6 +685,7 @@ export function createExpoIapVegaModule( } if ( + !response || !shouldRetryResponse('purchase-updates', response.responseCode) || attempt === PURCHASE_UPDATES_MAX_ATTEMPTS ) { @@ -806,7 +807,7 @@ export function createExpoIapVegaModule( fulfillmentResult: FULFILLMENT_RESULT_FULFILLED, receiptId: purchaseToken, }); - if (isSuccess('notify-fulfillment', response.responseCode)) return; + if (isSuccess('notify-fulfillment', response?.responseCode)) return; lastResponse = response; if (attempt < NOTIFY_FULFILLMENT_MAX_ATTEMPTS) { diff --git a/libraries/react-native-iap/src/vega-adapter.ts b/libraries/react-native-iap/src/vega-adapter.ts index f2e8df37..843849f1 100644 --- a/libraries/react-native-iap/src/vega-adapter.ts +++ b/libraries/react-native-iap/src/vega-adapter.ts @@ -733,6 +733,7 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { } if ( + !response || !shouldRetryResponse('purchase-updates', response.responseCode) || attempt === PURCHASE_UPDATES_MAX_ATTEMPTS ) { @@ -863,7 +864,7 @@ export function createVegaIapModule(service: VegaPurchasingService): RnIap { fulfillmentResult: FULFILLMENT_RESULT_FULFILLED, receiptId: purchaseToken, }); - if (isSuccess('notify-fulfillment', response.responseCode)) { + if (isSuccess('notify-fulfillment', response?.responseCode)) { return { responseCode: 0, code: '', From cb03fd0d701a0cab62e619b7817c69be4dcafe91 Mon Sep 17 00:00:00 2001 From: Hyo Date: Thu, 11 Jun 2026 16:19:51 +0900 Subject: [PATCH 47/51] fix(vega): isolate runtime dependencies Keep Amazon Vega dependencies out of normal example installs while preserving Vega build command discovery in generated targets. Clarify Expo and React Native Vega dependency boundaries in docs and catch native Fire OS purchase request failures in the example flow. --- libraries/expo-iap/example/app.config.ts | 3 +- libraries/expo-iap/example/bun.lock | 2069 ++--------------- libraries/expo-iap/example/package.json | 16 - .../example/scripts/build-vega-example.mjs | 50 +- .../expo-iap/plugin/__tests__/withIAP.test.ts | 78 +- libraries/expo-iap/plugin/src/withVega.ts | 65 +- libraries/expo-iap/src/ExpoIapModule.ts | 2 +- libraries/expo-iap/src/index.kepler.ts | 7 +- .../react-native-iap/src/index.kepler.ts | 2 +- libraries/react-native-iap/src/index.ts | 2 +- .../docs/src/pages/docs/features/vega-os.tsx | 103 +- packages/docs/src/pages/docs/setup/expo.tsx | 11 + .../src/pages/docs/setup/react-native.tsx | 66 +- .../hyo/martie/screens/PurchaseFlowScreen.kt | 103 +- 14 files changed, 509 insertions(+), 2068 deletions(-) diff --git a/libraries/expo-iap/example/app.config.ts b/libraries/expo-iap/example/app.config.ts index 027f8c58..b0f73bc0 100644 --- a/libraries/expo-iap/example/app.config.ts +++ b/libraries/expo-iap/example/app.config.ts @@ -23,6 +23,7 @@ const useLocalDev = export default ({config}: ConfigContext): ExpoConfig => { // Check if building for TV (set EXPO_TV=1 before prebuild) const isTV = process.env.EXPO_TV === '1'; + const isFireOsEnabled = process.env.EXPO_IAP_FIREOS === '1'; const isVegaEnabled = process.env.EXPO_IAP_VEGA === '1'; const isOnsideEnabled = false; @@ -50,7 +51,7 @@ export default ({config}: ConfigContext): ExpoConfig => { }, amazon: { // Fire OS: Android amazon flavor - fireOS: false, + fireOS: isFireOsEnabled, // Vega OS: Kepler runtime target vegaOS: isVegaEnabled, }, diff --git a/libraries/expo-iap/example/bun.lock b/libraries/expo-iap/example/bun.lock index 594ce31c..1b1f1b69 100644 --- a/libraries/expo-iap/example/bun.lock +++ b/libraries/expo-iap/example/bun.lock @@ -5,7 +5,6 @@ "": { "name": "expo-iap-example", "dependencies": { - "@amazon-devices/keplerscript-appstore-iap-lib": "~2.12.13", "@expo/react-native-action-sheet": "^4.1.1", "@expo/vector-icons": "^15.0.2", "@preact/signals-react": "^3.2.1", @@ -34,10 +33,6 @@ "react-native-webview": "13.15.0", }, "devDependencies": { - "@amazon-devices/kepler-cli-platform": "~0.22.0", - "@amazon-devices/kepler-compatibility-metro-config": "^0.0.6", - "@amazon-devices/kepler-module-resolver-preset": "^0.1.15", - "@amazon-devices/react-native-kepler": "^2.0.0", "@babel/core": "^7.25.2", "@react-native-community/cli": "11.3.2", "@react-native/metro-config": "^0.72.6", @@ -60,24 +55,6 @@ "packages": { "@0no-co/graphql.web": ["@0no-co/graphql.web@1.1.2", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-N2NGsU5FLBhT8NZ+3l2YrzZSHITjNXNuDhC4iDiikv0IujaJ0Xc6xIxQZ/Ek3Cb+rgPjnLHYyJm11tInuJn+cw=="], - "@amazon-devices/js-bundle-system-allow-list": ["@amazon-devices/js-bundle-system-allow-list@0.0.21", "", {}, "sha512-M2ule4za60h6MEAXDwmDhwK21m50bVrmT/16UIp9hXEpBy8dSHVdPYpV3xdvG0zTRY6dpyO5rsMCTd8mAVcuIw=="], - - "@amazon-devices/kepler-cli-platform": ["@amazon-devices/kepler-cli-platform@0.22.0", "", { "dependencies": { "@amazon-devices/kepler-compatibility-metro-config": "^0", "@amazon-devices/kepler-module-manifest-builder": "^0.1.0", "@react-native/js-polyfills": "^0.73.0", "@rnx-kit/tools-node": "^2.1.1", "find-workspaces": "^0.3.1", "lodash": "4.17.21", "ora": "^3.4.0", "semver": "7.7.1", "shelljs": "^0.8.5" }, "optionalDependencies": { "@amazon-devices/keplerscript-commonmodules": "^1.0.0" }, "peerDependencies": { "@babel/traverse": "^7.22.8", "@react-native-community/cli-tools": ">=11.3.2", "@react-native/metro-config": ">=0.72.6", "react-native": "*" } }, "sha512-Z/yl1ACTV4+JPRq98ptAAfT4s8pnmyuvY/BHXVsmqr6h33MzQPANI+c0HXRNZDO2UdarHcWncGnV6QgUAo83Rw=="], - - "@amazon-devices/kepler-compatibility-metro-config": ["@amazon-devices/kepler-compatibility-metro-config@0.0.6", "", {}, "sha512-sUsCC/1M6jXZB1Y1ya9apv3ObvYL73Bj3GRfmAnEg/NF6sXMtDVlboUv8JSxO53ixP+LOczvNDSzMo/ZNruEVg=="], - - "@amazon-devices/kepler-module-manifest-builder": ["@amazon-devices/kepler-module-manifest-builder@0.1.13", "", { "dependencies": { "lodash": "^4.17.21", "semver": "^7.0.0", "toml": "^3.0.0", "yargs": "^17.7.2" }, "optionalDependencies": { "@amazon-devices/js-bundle-system-allow-list": "^0" }, "bin": { "kepler-module-manifest-builder": "dist/src/cli.js" } }, "sha512-lQiw0fEKFjUQ/ZHWC/4j+TuAn9oJppIYa7f6x4gg7kaBy428U5fzOAO1VX0KeSyhMhAnD2uZeY+r6JgeNif88Q=="], - - "@amazon-devices/kepler-module-resolver-preset": ["@amazon-devices/kepler-module-resolver-preset@0.1.15", "", { "dependencies": { "@babel/core": "^7.20.0", "@babel/helper-plugin-utils": "^7.20.2" } }, "sha512-4oURGrzWVG9hxQ+2qO3+eWOG15Co4LZJ0hNpUxX9S5VIQj7mWuQ+N6T1xS1hI/9blIYpzyvUwR9ivztYhEJ62Q=="], - - "@amazon-devices/keplerscript-appstore-iap-lib": ["@amazon-devices/keplerscript-appstore-iap-lib@2.12.13", "", { "dependencies": { "@amazon-devices/keplerscript-turbomodule-api": "^1.0.0", "@amazon-devices/react-native-kepler": "^2.0.0", "react": "18.2.0", "react-native": "0.72.0", "react-native-uuid": "^2.0.1" } }, "sha512-O00hquk6y/oPxNVjd1NPZDMyZ1idiXb2nelFMjcZLfdnm4IrBCWZq/hovbQBzGy0gzsVe29ooMB8FTVp49fePQ=="], - - "@amazon-devices/keplerscript-commonmodules": ["@amazon-devices/keplerscript-commonmodules@1.0.1756501375", "", { "optionalDependencies": { "@amazon-devices/js-bundle-system-allow-list": "*" }, "bin": { "find-files": "findFiles.js", "validate-js-acr": "bin/validate-js-acr", "modify-allow-list": "bin/modifyAllowListScript.js", "compare-bundle-maps": "bin/compare-bundle-maps" } }, "sha512-+ZFaCRDhpMT6uBJq33g/0jZnUrXP58BRxLhdritVq5rjGOWy1ysveOv2WHvf85Bhyn82RuKdYqP6hmdGvOs0yg=="], - - "@amazon-devices/keplerscript-turbomodule-api": ["@amazon-devices/keplerscript-turbomodule-api@1.2.5", "", { "dependencies": { "@babel/parser": "^7.20.0", "flow-parser": "^0.206.0", "invariant": "^2.2.4", "jscodeshift": "^0.14.0", "nullthrows": "^1.1.1", "yargs": "^17.7.2" }, "bin": { "keplerscript-turbomodule-api": "cli.js" } }, "sha512-zg4vQ7iZLZkoajyjxYtGs293y3+FfpM5de63Xx6sPdIubNLh6Cjf5o7vxPnJ4+SnXOk4IlcSndnGIpJ7vtgJiw=="], - - "@amazon-devices/react-native-kepler": ["@amazon-devices/react-native-kepler@2.0.1758683737", "", { "dependencies": { "@babel/traverse": "7.23.2", "@jest/create-cache-key-function": "^29.2.1", "@react-native-community/cli": "11.4.1", "@react-native-community/cli-platform-android": "11.4.1", "@react-native-community/cli-platform-ios": "11.4.1", "@react-native/assets-registry": "^0.72.0", "@react-native/codegen": "^0.72.6", "@react-native/gradle-plugin": "^0.72.10", "@react-native/js-polyfills": "^0.72.1", "@react-native/normalize-colors": "^0.72.0", "@react-native/virtualized-lists": "^0.72.5", "abort-controller": "^3.0.0", "anser": "^1.4.9", "base64-js": "^1.1.2", "deprecated-react-native-prop-types": "4.1.0", "event-target-shim": "^5.0.1", "flow-enums-runtime": "^0.0.5", "invariant": "^2.2.4", "jest-environment-node": "^29.2.1", "jsc-android": "^250231.0.0", "memoize-one": "^5.0.0", "metro-runtime": "0.76.5", "metro-source-map": "0.76.5", "mkdirp": "^0.5.1", "nullthrows": "^1.1.1", "pretty-format": "^26.5.2", "promise": "^8.3.0", "react-devtools-core": "^4.27.2", "react-refresh": "^0.4.0", "react-shallow-renderer": "^16.15.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.24.0-canary-efb381bbf-20230505", "stacktrace-parser": "^0.1.10", "use-sync-external-store": "^1.0.0", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "react": "18.2.0", "react-native": "0.72.0" }, "bin": { "react-native-kepler": "cli.js" } }, "sha512-46QLiXYEKt4aQnnSV+ikIWeZEsVaO0UMNiyTs80odbk51REgiW1JsMcH1cA9f8wug2Dw4bLaGoQWTKi6ZAi4cw=="], - "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], @@ -100,12 +77,8 @@ "@babel/helper-environment-visitor": ["@babel/helper-environment-visitor@7.24.7", "", { "dependencies": { "@babel/types": "^7.24.7" } }, "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ=="], - "@babel/helper-function-name": ["@babel/helper-function-name@7.24.7", "", { "dependencies": { "@babel/template": "^7.24.7", "@babel/types": "^7.24.7" } }, "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA=="], - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], - "@babel/helper-hoist-variables": ["@babel/helper-hoist-variables@7.24.7", "", { "dependencies": { "@babel/types": "^7.24.7" } }, "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ=="], - "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA=="], "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], @@ -122,8 +95,6 @@ "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - "@babel/helper-split-export-declaration": ["@babel/helper-split-export-declaration@7.24.7", "", { "dependencies": { "@babel/types": "^7.24.7" } }, "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA=="], - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], @@ -138,18 +109,6 @@ "@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w=="], - - "@babel/plugin-bugfix-safari-class-field-initializer-scope": ["@babel/plugin-bugfix-safari-class-field-initializer-scope@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ=="], - - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ=="], - - "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": ["@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA=="], - - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-transform-optional-chaining": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw=="], - - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw=="], - "@babel/plugin-proposal-async-generator-functions": ["@babel/plugin-proposal-async-generator-functions@7.20.7", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-plugin-utils": "^7.20.2", "@babel/helper-remap-async-to-generator": "^7.18.9", "@babel/plugin-syntax-async-generators": "^7.8.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA=="], "@babel/plugin-proposal-class-properties": ["@babel/plugin-proposal-class-properties@7.18.6", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ=="], @@ -168,8 +127,6 @@ "@babel/plugin-proposal-optional-chaining": ["@babel/plugin-proposal-optional-chaining@7.21.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA=="], - "@babel/plugin-proposal-private-property-in-object": ["@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w=="], - "@babel/plugin-syntax-async-generators": ["@babel/plugin-syntax-async-generators@7.8.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw=="], "@babel/plugin-syntax-bigint": ["@babel/plugin-syntax-bigint@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg=="], @@ -186,9 +143,7 @@ "@babel/plugin-syntax-flow": ["@babel/plugin-syntax-flow@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA=="], - "@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw=="], - - "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg=="], + "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww=="], "@babel/plugin-syntax-import-meta": ["@babel/plugin-syntax-import-meta@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g=="], @@ -214,11 +169,9 @@ "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="], - "@babel/plugin-syntax-unicode-sets-regex": ["@babel/plugin-syntax-unicode-sets-regex@7.18.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg=="], - "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], - "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-remap-async-to-generator": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA=="], + "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q=="], "@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA=="], @@ -236,55 +189,33 @@ "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A=="], - "@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ=="], - - "@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ=="], - - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA=="], - - "@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg=="], - - "@babel/plugin-transform-explicit-resource-management": ["@babel/plugin-transform-explicit-resource-management@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-transform-destructuring": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw=="], - - "@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ=="], - "@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ=="], "@babel/plugin-transform-flow-strip-types": ["@babel/plugin-transform-flow-strip-types@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-flow": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg=="], - "@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ=="], + "@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw=="], "@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.27.1", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ=="], - "@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg=="], - "@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA=="], - "@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q=="], + "@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw=="], "@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg=="], - "@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew=="], - "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], - "@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ=="], - - "@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA=="], - "@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng=="], - "@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A=="], - "@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA=="], - "@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw=="], + "@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw=="], "@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.28.0", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/traverse": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA=="], "@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA=="], - "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng=="], + "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q=="], "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg=="], @@ -308,11 +239,7 @@ "@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA=="], - "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw=="], - - "@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ=="], - - "@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA=="], + "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.28.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg=="], "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.28.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-dGopk9nZrtCs2+nfIem25UuHyt5moSJamArzIoh9/vezUQPmYDOzjaHDCkAzuGJibCIkPup8rMT2+wYB6S73cA=="], @@ -324,30 +251,14 @@ "@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg=="], - "@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A=="], - "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.0", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg=="], - "@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA=="], - - "@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw=="], - "@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw=="], - "@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg=="], - - "@babel/preset-env": ["@babel/preset-env@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.29.7", "@babel/plugin-syntax-import-attributes": "^7.29.7", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.29.7", "@babel/plugin-transform-async-generator-functions": "^7.29.7", "@babel/plugin-transform-async-to-generator": "^7.29.7", "@babel/plugin-transform-block-scoped-functions": "^7.29.7", "@babel/plugin-transform-block-scoping": "^7.29.7", "@babel/plugin-transform-class-properties": "^7.29.7", "@babel/plugin-transform-class-static-block": "^7.29.7", "@babel/plugin-transform-classes": "^7.29.7", "@babel/plugin-transform-computed-properties": "^7.29.7", "@babel/plugin-transform-destructuring": "^7.29.7", "@babel/plugin-transform-dotall-regex": "^7.29.7", "@babel/plugin-transform-duplicate-keys": "^7.29.7", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", "@babel/plugin-transform-dynamic-import": "^7.29.7", "@babel/plugin-transform-explicit-resource-management": "^7.29.7", "@babel/plugin-transform-exponentiation-operator": "^7.29.7", "@babel/plugin-transform-export-namespace-from": "^7.29.7", "@babel/plugin-transform-for-of": "^7.29.7", "@babel/plugin-transform-function-name": "^7.29.7", "@babel/plugin-transform-json-strings": "^7.29.7", "@babel/plugin-transform-literals": "^7.29.7", "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", "@babel/plugin-transform-member-expression-literals": "^7.29.7", "@babel/plugin-transform-modules-amd": "^7.29.7", "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-modules-systemjs": "^7.29.7", "@babel/plugin-transform-modules-umd": "^7.29.7", "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", "@babel/plugin-transform-new-target": "^7.29.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", "@babel/plugin-transform-numeric-separator": "^7.29.7", "@babel/plugin-transform-object-rest-spread": "^7.29.7", "@babel/plugin-transform-object-super": "^7.29.7", "@babel/plugin-transform-optional-catch-binding": "^7.29.7", "@babel/plugin-transform-optional-chaining": "^7.29.7", "@babel/plugin-transform-parameters": "^7.29.7", "@babel/plugin-transform-private-methods": "^7.29.7", "@babel/plugin-transform-private-property-in-object": "^7.29.7", "@babel/plugin-transform-property-literals": "^7.29.7", "@babel/plugin-transform-regenerator": "^7.29.7", "@babel/plugin-transform-regexp-modifiers": "^7.29.7", "@babel/plugin-transform-reserved-words": "^7.29.7", "@babel/plugin-transform-shorthand-properties": "^7.29.7", "@babel/plugin-transform-spread": "^7.29.7", "@babel/plugin-transform-sticky-regex": "^7.29.7", "@babel/plugin-transform-template-literals": "^7.29.7", "@babel/plugin-transform-typeof-symbol": "^7.29.7", "@babel/plugin-transform-unicode-escapes": "^7.29.7", "@babel/plugin-transform-unicode-property-regex": "^7.29.7", "@babel/plugin-transform-unicode-regex": "^7.29.7", "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.15", "babel-plugin-polyfill-corejs3": "^0.14.0", "babel-plugin-polyfill-regenerator": "^0.6.6", "core-js-compat": "^3.48.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA=="], - - "@babel/preset-flow": ["@babel/preset-flow@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-transform-flow-strip-types": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KYIRV0BuaN68CDdsqFkAD7MU7yipUqQNuNElwATdxaIdpTjhvtY82QvkBJs7zV3Evxj2jFAAZ1iO8nyy0nhjqA=="], - - "@babel/preset-modules": ["@babel/preset-modules@0.1.6-no-external-plugins", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA=="], - "@babel/preset-react": ["@babel/preset-react@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-transform-react-display-name": "^7.27.1", "@babel/plugin-transform-react-jsx": "^7.27.1", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@babel/plugin-transform-react-pure-annotations": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA=="], "@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="], - "@babel/register": ["@babel/register@7.29.7", "", { "dependencies": { "clone-deep": "^4.0.1", "find-cache-dir": "^2.0.0", "make-dir": "^2.1.0", "pirates": "^4.0.6", "source-map-support": "^0.5.16" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-AMGJoWuES861riy6pcB0fphE1YXybtQnBYQMuIyPv6mKLiosfa79BKTnAOyx215c/3RJPJpdQwoHZ3earVH7AA=="], - "@babel/runtime": ["@babel/runtime@7.27.6", "", {}, "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q=="], "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], @@ -472,12 +383,6 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], "@preact/signals-core": ["@preact/signals-core@1.11.0", "", {}, "sha512-jglbibeWHuFRzEWVFY/TT7wB1PppJxmcSfUHcK+2J9vBRtiooMfw6tAPttojNYrrpdGViqAYCbPpmWYlMm+eMQ=="], @@ -538,15 +443,15 @@ "@react-native-community/cli-hermes": ["@react-native-community/cli-hermes@11.3.2", "", { "dependencies": { "@react-native-community/cli-platform-android": "11.3.2", "@react-native-community/cli-tools": "11.3.2", "chalk": "^4.1.2", "hermes-profile-transformer": "^0.0.6", "ip": "^1.1.5" } }, "sha512-IfzdYTjxu+BFEvweY9TXpLkOmWq0sxK8PTN+u0BduiT9cJRvcO0CxjOpLHAabVrSJc6o+7aLfEvogBmdN53Xfg=="], - "@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@11.4.1", "", { "dependencies": { "@react-native-community/cli-tools": "11.4.1", "chalk": "^4.1.2", "execa": "^5.0.0", "glob": "^7.1.3", "logkitty": "^0.7.1" } }, "sha512-VMmXWIzk0Dq5RAd+HIEa3Oe7xl2jso7+gOr6E2HALF4A3fCKUjKZQ6iK2t6AfnY04zftvaiKw6zUXtrfl52AVQ=="], + "@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@11.3.2", "", { "dependencies": { "@react-native-community/cli-tools": "11.3.2", "chalk": "^4.1.2", "execa": "^5.0.0", "glob": "^7.1.3", "logkitty": "^0.7.1" } }, "sha512-NKxyBP0/gwL4/tNWrkevFSjeb7Dw2SByNfE9wFXBaAvZHxbxxJUjZOTOW3ueOXEpgOMU7IYYOiSOz2M10IRQ2A=="], - "@react-native-community/cli-platform-ios": ["@react-native-community/cli-platform-ios@11.4.1", "", { "dependencies": { "@react-native-community/cli-tools": "11.4.1", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-xml-parser": "^4.0.12", "glob": "^7.1.3", "ora": "^5.4.1" } }, "sha512-RPhwn+q3IY9MpWc9w/Qmzv03mo8sXdah2eSZcECgweqD5SHWtOoRCUt11zM8jASpAQ8Tm5Je7YE9bHvdwGl4hA=="], + "@react-native-community/cli-platform-ios": ["@react-native-community/cli-platform-ios@11.3.2", "", { "dependencies": { "@react-native-community/cli-tools": "11.3.2", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-xml-parser": "^4.0.12", "glob": "^7.1.3", "ora": "^5.4.1" } }, "sha512-XPrfsI7dNY3f9crsKDDRIss+GHYX/snuYfMrjg4ZBHpYB5JdLepO8FJ5bFz+/s9KXDm045ijo8QFcIf3XJR0YQ=="], "@react-native-community/cli-plugin-metro": ["@react-native-community/cli-plugin-metro@11.3.2", "", { "dependencies": { "@react-native-community/cli-server-api": "11.3.2", "@react-native-community/cli-tools": "11.3.2", "chalk": "^4.1.2", "execa": "^5.0.0", "metro": "0.76.5", "metro-config": "0.76.5", "metro-core": "0.76.5", "metro-react-native-babel-transformer": "0.76.5", "metro-resolver": "0.76.5", "metro-runtime": "0.76.5", "readline": "^1.3.0" } }, "sha512-r1rZYCFfxZWIiUlukjMcDlxfCtm+QNYu+vFyVfE9yvN9gaNPBAi9029eVzkRkFuJ8Rxwr67HnYEAdGYLWQ1uIw=="], "@react-native-community/cli-server-api": ["@react-native-community/cli-server-api@11.3.2", "", { "dependencies": { "@react-native-community/cli-debugger-ui": "11.3.2", "@react-native-community/cli-tools": "11.3.2", "compression": "^1.7.1", "connect": "^3.6.5", "errorhandler": "^1.5.1", "nocache": "^3.0.1", "pretty-format": "^26.6.2", "serve-static": "^1.13.1", "ws": "^7.5.1" } }, "sha512-6rMb37HYWOdmiMGCxsttHDLIP7KmcJjWvzTJzb2tm9P5FoMvSSmSOn981MuP835Lk1U+IdjVcwtsA247Im4mkg=="], - "@react-native-community/cli-tools": ["@react-native-community/cli-tools@11.4.1", "", { "dependencies": { "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "find-up": "^5.0.0", "mime": "^2.4.1", "node-fetch": "^2.6.0", "open": "^6.2.0", "ora": "^5.4.1", "semver": "^7.5.2", "shell-quote": "^1.7.3" } }, "sha512-GuQIuY/kCPfLeXB1aiPZ5HvF+e/wdO42AYuNEmT7FpH/0nAhdTxA9qjL8m3vatDD2/YK7WNOSVNsl2UBZuOISg=="], + "@react-native-community/cli-tools": ["@react-native-community/cli-tools@11.3.2", "", { "dependencies": { "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "find-up": "^5.0.0", "mime": "^2.4.1", "node-fetch": "^2.6.0", "open": "^6.2.0", "ora": "^5.4.1", "semver": "^6.3.0", "shell-quote": "^1.7.3" } }, "sha512-rAnFPzRITeEhBLwC73ucvWsYjsGyotSOI4c+k8t9wUqcIk1Q+RFnuWozGc13msOPdESvBHt2MPJBwXrtKgKn1g=="], "@react-native-community/cli-types": ["@react-native-community/cli-types@11.3.2", "", { "dependencies": { "joi": "^17.2.1" } }, "sha512-jba1Z1YgC4JIHPADSqpl4ATsrJaOja1zlQCbH/yE8McHRjVBzeYGeHIvG5jw7iU5cw6FFifH5vvr23JPGk8oyw=="], @@ -554,13 +459,13 @@ "@react-native-tvos/virtualized-lists": ["@react-native-tvos/virtualized-lists@0.81.5-1", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.1.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-v77jJvzH2jzMj3G8pthdaRjiUhmdQ3S/OGiTX45Tn1J+whLaPOEkVRCel9xPHhrTPIEwrOOwGNiAFN/s1hzWZA=="], - "@react-native/assets-registry": ["@react-native/assets-registry@0.72.0", "", {}, "sha512-Im93xRJuHHxb1wniGhBMsxLwcfzdYreSZVQGDoMJgkd6+Iky61LInGEHnQCTN0fKNYF1Dvcofb4uMmE1RQHXHQ=="], + "@react-native/assets-registry": ["@react-native/assets-registry@0.81.5", "", {}, "sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w=="], "@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.81.4", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.81.4" } }, "sha512-6ztXf2Tl2iWznyI/Da/N2Eqymt0Mnn69GCLnEFxFbNdk0HxHPZBNWU9shTXhsLWOL7HATSqwg/bB1+3kY1q+mA=="], "@react-native/babel-preset": ["@react-native/babel-preset@0.81.4", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.81.4", "babel-plugin-syntax-hermes-parser": "0.29.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-VYj0c/cTjQJn/RJ5G6P0L9wuYSbU9yGbPYDHCKstlQZQWkk+L9V8ZDbxdJBTIei9Xl3KPQ1odQ4QaeW+4v+AZg=="], - "@react-native/codegen": ["@react-native/codegen@0.72.8", "", { "dependencies": { "@babel/parser": "^7.20.0", "flow-parser": "^0.206.0", "glob": "^7.1.1", "invariant": "^2.2.4", "jscodeshift": "^0.14.0", "mkdirp": "^0.5.1", "nullthrows": "^1.1.1" }, "peerDependencies": { "@babel/preset-env": "^7.1.6" } }, "sha512-jQCcBlXV7B7ap5VlHhwIPieYz89yiRgwd2FPUBu+unz+kcJ6pAiB2U8RdLDmyIs8fiWd+Vq1xxaWs4TR329/ng=="], + "@react-native/codegen": ["@react-native/codegen@0.81.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.29.1", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-a2TDA03Up8lpSa9sh5VRGCQDXgCTOyDOFH+aqyinxp1HChG8uk89/G+nkJ9FPd0rqgi25eCTR16TWdS3b+fA6g=="], "@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.81.5", "", { "dependencies": { "@react-native/dev-middleware": "0.81.5", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.83.1", "metro-config": "^0.83.1", "metro-core": "^0.83.1", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli": "*", "@react-native/metro-config": "*" }, "optionalPeers": ["@react-native-community/cli", "@react-native/metro-config"] }, "sha512-yWRlmEOtcyvSZ4+OvqPabt+NS36vg0K/WADTQLhrYrm9qdZSuXmq8PmdJWz/68wAqKQ+4KTILiq2kjRQwnyhQw=="], @@ -568,15 +473,13 @@ "@react-native/dev-middleware": ["@react-native/dev-middleware@0.81.4", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.81.4", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^6.2.3" } }, "sha512-hu1Wu5R28FT7nHXs2wWXvQ++7W7zq5GPY83llajgPlYKznyPLAY/7bArc5rAzNB7b0kwnlaoPQKlvD/VP9LZug=="], - "@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.72.11", "", {}, "sha512-P9iRnxiR2w7EHcZ0mJ+fmbPzMby77ZzV6y9sJI3lVLJzF7TLSdbwcQyD3lwMsiL+q5lKUHoZJS4sYmih+P2HXw=="], + "@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.81.5", "", {}, "sha512-hORRlNBj+ReNMLo9jme3yQ6JQf4GZpVEBLxmTXGGlIL78MAezDZr5/uq9dwElSbcGmLEgeiax6e174Fie6qPLg=="], - "@react-native/js-polyfills": ["@react-native/js-polyfills@0.73.1", "", {}, "sha512-ewMwGcumrilnF87H4jjrnvGZEaPFCAC4ebraEK+CurDDmwST/bIicI4hrOAv+0Z0F7DEK4O4H7r8q9vH7IbN4g=="], + "@react-native/js-polyfills": ["@react-native/js-polyfills@0.72.1", "", {}, "sha512-cRPZh2rBswFnGt5X5EUEPs0r+pAsXxYsifv/fgy9ZLQokuT52bPH+9xjDR+7TafRua5CttGW83wP4TntRcWNDA=="], "@react-native/metro-config": ["@react-native/metro-config@0.72.12", "", { "dependencies": { "@react-native/js-polyfills": "^0.72.1", "metro-config": "^0.76.9", "metro-react-native-babel-transformer": "^0.76.9", "metro-runtime": "^0.76.9" } }, "sha512-6NC5nr70oV8gH5vTz0yVYig6TGn97NfE58DdYottuOGPEODZf9Jpb7gdLs6Rqj5ryFDsKVPU3NsFmXKBJwEgXQ=="], - "@react-native/normalize-colors": ["@react-native/normalize-colors@0.72.0", "", {}, "sha512-285lfdqSXaqKuBbbtP9qL2tDrfxdOFtIMvkKadtleRQkdOxx+uzGvFr82KHmc/sSiMtfXGp7JnFYWVh4sFl7Yw=="], - - "@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.72.8", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "react-native": "*" } }, "sha512-J3Q4Bkuo99k7mu+jPS9gSUSgq+lLRSI/+ahXNwV92XgJ/8UgOTxu2LPwhJnBk/sQKxq7E8WkZBnBiozukQMqrw=="], + "@react-native/normalize-colors": ["@react-native/normalize-colors@0.81.4", "", {}, "sha512-9nRRHO1H+tcFqjb9gAM105Urtgcanbta2tuqCVY0NATHeFPDEAB7gPyiLxCHKMi1NbhP6TH0kxgSWXKZl1cyRg=="], "@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.4.2", "", { "dependencies": { "@react-navigation/elements": "^2.5.2", "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": "^7.1.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-jyBux5l3qqEucY5M/ZWxVvfA8TQu7DVl2gK+xB6iKqRUfLf7dSumyVxc7HemDwGFoz3Ug8dVZFvSMEs+mfrieQ=="], @@ -590,8 +493,6 @@ "@react-navigation/routers": ["@react-navigation/routers@7.4.1", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-42mZrMzQ0LfKxUb5OHIurYrPYyRsXFLolucILrvm21f0O40Sw0Ufh1bnn/jRqnxZZu7wvpUGIGYM8nS9zVE1Aw=="], - "@rnx-kit/tools-node": ["@rnx-kit/tools-node@2.1.2", "", {}, "sha512-pCpiUpC/032ZoN4iFZFWtKp3Vrjma115nXwv2gyD2XFxj6DFyTX6pYjSK70xT7gwLMU0C3bZonN1JxiOBlGb0A=="], - "@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="], "@sideway/formula": ["@sideway/formula@3.0.1", "", {}, "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="], @@ -750,8 +651,6 @@ "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], - "ast-types": ["ast-types@0.15.2", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg=="], - "astral-regex": ["astral-regex@1.0.0", "", {}, "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg=="], "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], @@ -760,8 +659,6 @@ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - "babel-core": ["babel-core@7.0.0-bridge.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg=="], - "babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="], "babel-plugin-istanbul": ["babel-plugin-istanbul@6.1.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA=="], @@ -798,8 +695,6 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.34", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw=="], - "better-opn": ["better-opn@3.0.2", "", { "dependencies": { "open": "^8.0.4" } }, "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ=="], "big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="], @@ -852,7 +747,7 @@ "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], - "cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], + "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], @@ -862,8 +757,6 @@ "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], - "clone-deep": ["clone-deep@4.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", "shallow-clone": "^3.0.0" } }, "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ=="], - "co": ["co@4.6.0", "", {}, "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ=="], "collect-v8-coverage": ["collect-v8-coverage@1.0.2", "", {}, "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q=="], @@ -884,21 +777,17 @@ "commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], - "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], - "compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="], "compression": ["compression@1.8.0", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.0.2", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA=="], "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], - "connect": ["connect@3.7.0", "", { "dependencies": { "debug": "2.6.9", "finalhandler": "1.1.2", "parseurl": "~1.3.3", "utils-merge": "1.0.1" } }, "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "core-js-compat": ["core-js-compat@3.49.0", "", { "dependencies": { "browserslist": "^4.28.1" } }, "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA=="], + "core-js-compat": ["core-js-compat@3.44.0", "", { "dependencies": { "browserslist": "^4.25.1" } }, "sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA=="], "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], @@ -944,8 +833,6 @@ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - "deprecated-react-native-prop-types": ["deprecated-react-native-prop-types@4.1.0", "", { "dependencies": { "@react-native/normalize-colors": "*", "invariant": "*", "prop-types": "*" } }, "sha512-WfepZHmRbbdTvhcolb8aOKEvQdcmTMn5tKLbqbXmkBvjFjRVWAYqsXk/DBsV8TZxws8SdGHLuHaJrHSQUPRdfw=="], - "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], "detect-libc": ["detect-libc@2.1.1", "", {}, "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw=="], @@ -1072,16 +959,12 @@ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="], "fast-xml-parser": ["fast-xml-parser@4.5.6", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A=="], - "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - "fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -1092,15 +975,9 @@ "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="], - "find-cache-dir": ["find-cache-dir@2.1.0", "", { "dependencies": { "commondir": "^1.0.1", "make-dir": "^2.0.0", "pkg-dir": "^3.0.0" } }, "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ=="], - "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], - "find-workspaces": ["find-workspaces@0.3.1", "", { "dependencies": { "fast-glob": "^3.3.2", "pkg-types": "^1.0.3", "yaml": "^2.3.4" } }, "sha512-UDkGILGJSA1LN5Aa7McxCid4sqW3/e+UYsVwyxki3dDT0F8+ym0rAfnCkEfkL0rO7M+8/mvkim4t/s3IPHmg+w=="], - - "flow-enums-runtime": ["flow-enums-runtime@0.0.5", "", {}, "sha512-PSZF9ZuaZD03sT9YaIs0FrGJ7lSUw7rHZIex+73UYVXg46eL/wxN5PaVcPJFudE2cJu5f0fezitV5aBkLHPUOQ=="], - - "flow-parser": ["flow-parser@0.206.0", "", {}, "sha512-HVzoK3r6Vsg+lKvlIZzaWNBVai+FXTX1wdYhz/wVlH13tb/gOdLXmlTqy6odmTBhT5UoWUbq0k8263Qhr9d88w=="], + "flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], "fontfaceobserver": ["fontfaceobserver@2.3.0", "", {}, "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg=="], @@ -1138,14 +1015,10 @@ "glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], - "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], "global-dirs": ["global-dirs@0.1.1", "", { "dependencies": { "ini": "^1.3.4" } }, "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg=="], - "globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], @@ -1204,8 +1077,6 @@ "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - "interpret": ["interpret@1.4.0", "", {}, "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="], - "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], "ip": ["ip@1.1.9", "", {}, "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ=="], @@ -1218,20 +1089,14 @@ "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "is-generator-fn": ["is-generator-fn@2.1.0", "", {}, "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ=="], - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - "is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="], - "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], @@ -1244,8 +1109,6 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="], - "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], "istanbul-lib-instrument": ["istanbul-lib-instrument@5.2.1", "", { "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" } }, "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg=="], @@ -1326,12 +1189,8 @@ "js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], - "jsc-android": ["jsc-android@250231.0.0", "", {}, "sha512-rS46PvsjYmdmuz1OAWXY/1kCYG7pnf1TBqeTiOJr1iDz7s5DLxxC9n/ZMknLDxzYzNVfI7R95MH10emSSG1Wuw=="], - "jsc-safe-url": ["jsc-safe-url@0.2.4", "", {}, "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q=="], - "jscodeshift": ["jscodeshift@0.14.0", "", { "dependencies": { "@babel/core": "^7.13.16", "@babel/parser": "^7.13.16", "@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", "@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/plugin-transform-modules-commonjs": "^7.13.8", "@babel/preset-flow": "^7.13.13", "@babel/preset-typescript": "^7.13.0", "@babel/register": "^7.13.16", "babel-core": "^7.0.0-bridge.0", "chalk": "^4.1.2", "flow-parser": "0.*", "graceful-fs": "^4.2.4", "micromatch": "^4.0.4", "neo-async": "^2.5.0", "node-dir": "^0.1.17", "recast": "^0.21.0", "temp": "^0.8.4", "write-file-atomic": "^2.3.0" }, "peerDependencies": { "@babel/preset-env": "^7.1.6" }, "bin": { "jscodeshift": "bin/jscodeshift.js" } }, "sha512-7eCC1knD7bLUPuSCwXsMZUH51O8jIcoVyKtI6P0XM0IVzlGjckPy3FIwQlorzbN0Sg79oK+RlohN32Mqf/lrYA=="], - "jsdom": ["jsdom@20.0.3", "", { "dependencies": { "abab": "^2.0.6", "acorn": "^8.8.1", "acorn-globals": "^7.0.0", "cssom": "^0.5.0", "cssstyle": "^2.3.0", "data-urls": "^3.0.2", "decimal.js": "^10.4.2", "domexception": "^4.0.0", "escodegen": "^2.0.0", "form-data": "^4.0.0", "html-encoding-sniffer": "^3.0.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.1", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.2", "parse5": "^7.1.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^4.1.2", "w3c-xmlserializer": "^4.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^2.0.0", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^11.0.0", "ws": "^8.11.0", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "canvas": "^2.5.0" }, "optionalPeers": ["canvas"] }, "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], @@ -1346,8 +1205,6 @@ "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], - "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], - "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], "lan-network": ["lan-network@0.1.7", "", { "bin": { "lan-network": "dist/lan-network-cli.js" } }, "sha512-mnIlAEMu4OyEvUNdzco9xpuB9YVcPkQec+QsgycBCtPZvEqWPCDPfbAE4OJMdBBWpZWtpCn1xw9jJYlwjWI5zQ=="], @@ -1390,7 +1247,7 @@ "lodash.throttle": ["lodash.throttle@4.1.1", "", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="], - "log-symbols": ["log-symbols@2.2.0", "", { "dependencies": { "chalk": "^2.0.1" } }, "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg=="], + "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], "logkitty": ["logkitty@0.7.1", "", { "dependencies": { "ansi-fragments": "^0.2.1", "dayjs": "^1.8.15", "yargs": "^15.1.0" }, "bin": { "logkitty": "bin/logkitty.js" } }, "sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ=="], @@ -1398,7 +1255,7 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "make-dir": ["make-dir@2.1.0", "", { "dependencies": { "pify": "^4.0.1", "semver": "^5.6.0" } }, "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA=="], + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="], @@ -1410,8 +1267,6 @@ "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - "metro": ["metro@0.76.5", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "@babel/core": "^7.20.0", "@babel/generator": "^7.20.0", "@babel/parser": "^7.20.0", "@babel/template": "^7.0.0", "@babel/traverse": "^7.20.0", "@babel/types": "^7.20.0", "accepts": "^1.3.7", "async": "^3.2.2", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^2.2.0", "denodeify": "^1.2.1", "error-stack-parser": "^2.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.8.0", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^27.2.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.76.5", "metro-cache": "0.76.5", "metro-cache-key": "0.76.5", "metro-config": "0.76.5", "metro-core": "0.76.5", "metro-file-map": "0.76.5", "metro-inspector-proxy": "0.76.5", "metro-minify-terser": "0.76.5", "metro-minify-uglify": "0.76.5", "metro-react-native-babel-preset": "0.76.5", "metro-resolver": "0.76.5", "metro-runtime": "0.76.5", "metro-source-map": "0.76.5", "metro-symbolicate": "0.76.5", "metro-transform-plugins": "0.76.5", "metro-transform-worker": "0.76.5", "mime-types": "^2.1.27", "node-fetch": "^2.2.0", "nullthrows": "^1.1.1", "rimraf": "^3.0.2", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "strip-ansi": "^6.0.0", "throat": "^5.0.0", "ws": "^7.5.1", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-aEQiqNFibfx4ajUXm7Xatsv43r/UQ0xE53T3XqgZBzsxhF235tf1cl8t0giawi0RbLtDS+Fu4kg2bVBKDYFy7A=="], "metro-babel-transformer": ["metro-babel-transformer@0.83.1", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.29.1", "nullthrows": "^1.1.1" } }, "sha512-r3xAD3964E8dwDBaZNSO2aIIvWXjIK80uO2xo0/pi3WI8XWT9h5SCjtGWtMtE5PRWw+t20TN0q1WMRsjvhC1rQ=="], @@ -1438,11 +1293,11 @@ "metro-resolver": ["metro-resolver@0.76.5", "", {}, "sha512-QNsbDdf0xL1HefP6fhh1g3umqiX1qWEuCiBaTFroYRqM7u7RATt8mCu4n/FwSYhATuUUujHTIb2EduuQPbSGRQ=="], - "metro-runtime": ["metro-runtime@0.76.5", "", { "dependencies": { "@babel/runtime": "^7.0.0", "react-refresh": "^0.4.0" } }, "sha512-1JAf9/v/NDHLhoTfiJ0n25G6dRkX7mjTkaMJ6UUXIyfIuSucoK5yAuOBx8OveNIekoLRjmyvSmyN5ojEeRmpvQ=="], + "metro-runtime": ["metro-runtime@0.76.9", "", { "dependencies": { "@babel/runtime": "^7.0.0", "react-refresh": "^0.4.0" } }, "sha512-/5vezDpGUtA0Fv6cJg0+i6wB+QeBbvLeaw9cTSG7L76liP0b91f8vOcYzGaUbHI8pznJCCTerxRzpQ8e3/NcDw=="], - "metro-source-map": ["metro-source-map@0.76.5", "", { "dependencies": { "@babel/traverse": "^7.20.0", "@babel/types": "^7.20.0", "invariant": "^2.2.4", "metro-symbolicate": "0.76.5", "nullthrows": "^1.1.1", "ob1": "0.76.5", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-1EhYPcoftONlvnOzgos7daE8hsJKOgSN3nD3Xf/yaY1F0aLeGeuWfpiNLLeFDNyUhfObHSuNxNhDQF/x1GFEbw=="], + "metro-source-map": ["metro-source-map@0.83.1", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.1", "nullthrows": "^1.1.1", "ob1": "0.83.1", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-De7Vbeo96fFZ2cqmI0fWwVJbtHIwPZv++LYlWSwzTiCzxBDJORncN0LcT48Vi2UlQLzXJg+/CuTAcy7NBVh69A=="], - "metro-symbolicate": ["metro-symbolicate@0.76.5", "", { "dependencies": { "invariant": "^2.2.4", "metro-source-map": "0.76.5", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "through2": "^2.0.1", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-7iftzh6G6HO4UDBmjsi2Yu4d6IkApv6Kg+jmBvkTjCXr8HwnKKum89gMg/FRMix+Rhhut0dnMpz6mAbtKTU9JQ=="], + "metro-symbolicate": ["metro-symbolicate@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.1", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-wPxYkONlq/Sv8Ji7vHEx5OzFouXAMQJjpcPW41ySKMLP/Ir18SsiJK2h4YkdKpYrTS1+0xf8oqF6nxCsT3uWtg=="], "metro-transform-plugins": ["metro-transform-plugins@0.83.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-1Y+I8oozXwhuS0qwC+ezaHXBf0jXW4oeYn4X39XWbZt9X2HfjodqY9bH9r6RUTsoiK7S4j8Ni2C91bUC+sktJQ=="], @@ -1468,9 +1323,7 @@ "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], - "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], - - "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], + "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -1490,8 +1343,6 @@ "node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="], - "node-dir": ["node-dir@0.1.17", "", { "dependencies": { "minimatch": "^3.0.2" } }, "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg=="], - "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "node-forge": ["node-forge@1.3.1", "", {}, "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="], @@ -1512,7 +1363,7 @@ "nwsapi": ["nwsapi@2.2.20", "", {}, "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA=="], - "ob1": ["ob1@0.76.5", "", {}, "sha512-HoxZXMXNuY/eIXGoX7gx1C4O3eB4kJJMola6KoFaMm7PGGg39+AnhbgMASYVmSvP2lwU3545NyiR63g8J9PW3w=="], + "ob1": ["ob1@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-ngwqewtdUzFyycomdbdIhFLjePPSOt1awKMUXQ0L7iLHgWEPF3DsCerblzjzfAUHaXuvE9ccJymWQ/4PNNqvnQ=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -1526,7 +1377,7 @@ "open": ["open@6.4.0", "", { "dependencies": { "is-wsl": "^1.1.0" } }, "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg=="], - "ora": ["ora@3.4.0", "", { "dependencies": { "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-spinners": "^2.0.0", "log-symbols": "^2.2.0", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1" } }, "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg=="], + "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -1554,20 +1405,14 @@ "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@3.0.1", "", {}, "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag=="], - "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], - "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], - "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], - "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], @@ -1578,7 +1423,7 @@ "pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], - "pretty-format": ["pretty-format@26.6.2", "", { "dependencies": { "@jest/types": "^26.6.2", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^17.0.1" } }, "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg=="], + "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], "proc-log": ["proc-log@4.2.0", "", {}, "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA=="], @@ -1590,8 +1435,6 @@ "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], - "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], - "psl": ["psl@1.15.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], @@ -1606,8 +1449,6 @@ "queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="], - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], @@ -1616,7 +1457,7 @@ "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], - "react-devtools-core": ["react-devtools-core@4.28.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA=="], + "react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="], "react-dom": ["react-dom@19.0.0-rc-6230622a1a-20240610", "", { "dependencies": { "scheduler": "0.25.0-rc-6230622a1a-20240610" }, "peerDependencies": { "react": "19.0.0-rc-6230622a1a-20240610" } }, "sha512-56G4Pum5E7FeGL1rwHX5IxidSJxQnXP4yORRo0pVeOJuu5DQJvNKpUwmJoftMP/ez0AiglYTY77L2Gs8iyt1Hg=="], @@ -1640,8 +1481,6 @@ "react-native-screens": ["react-native-screens@4.16.0", "", { "dependencies": { "react-freeze": "^1.0.0", "react-native-is-edge-to-edge": "^1.2.1", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q=="], - "react-native-uuid": ["react-native-uuid@2.0.4", "", {}, "sha512-LSJNeh559qC17fgVPBsWuTSW/OygFp2dwTcf94IQBLYft5FzIQS9pCsuT36OPvyvDOMb6yiGr6TafaJDnz9PPQ=="], - "react-native-webview": ["react-native-webview@13.15.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "invariant": "2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ=="], "react-native-worklets": ["react-native-worklets@0.6.0", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "semver": "7.7.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, "sha512-yETMNuCcivdYWteuG4eRqgiAk2DzRCrVAaEBIEWPo4emrf3BNjadFo85L5QvyEusrX9QKE3ZEAx8U5A/nbyFgg=="], @@ -1654,8 +1493,6 @@ "react-server-dom-webpack": ["react-server-dom-webpack@19.0.0", "", { "dependencies": { "acorn-loose": "^8.3.0", "neo-async": "^2.6.1", "webpack-sources": "^3.2.0" }, "peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", "webpack": "^5.59.0" } }, "sha512-hLug9KEXLc8vnU9lDNe2b2rKKDaqrp5gNiES4uyu2Up3FZfZJZmdwLFXlWzdA9gTB/6/cWduSB2K1Lfag2pSvw=="], - "react-shallow-renderer": ["react-shallow-renderer@16.15.0", "", { "dependencies": { "object-assign": "^4.1.1", "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA=="], - "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], "react-test-renderer": ["react-test-renderer@19.1.0", "", { "dependencies": { "react-is": "^19.1.0", "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-jXkSl3CpvPYEF+p/eGDLB4sPoDX8pKkYvRl9+rR8HxLY0X04vW7hCm1/0zHoUSjPZ3bDa+wXWNTDVIw/R8aDVw=="], @@ -1664,10 +1501,6 @@ "readline": ["readline@1.3.0", "", {}, "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg=="], - "recast": ["recast@0.21.5", "", { "dependencies": { "ast-types": "0.15.2", "esprima": "~4.0.0", "source-map": "~0.6.1", "tslib": "^2.0.1" } }, "sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg=="], - - "rechoir": ["rechoir@0.6.2", "", { "dependencies": { "resolve": "^1.1.6" } }, "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw=="], - "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], "regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], @@ -1706,14 +1539,10 @@ "resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="], - "restore-cursor": ["restore-cursor@2.0.0", "", { "dependencies": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" } }, "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q=="], - - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], @@ -1722,11 +1551,11 @@ "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], - "scheduler": ["scheduler@0.24.0-canary-efb381bbf-20230505", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-ABvovCDe/k9IluqSh4/ISoq8tIJnW8euVAWYt5j/bg6dRnqwQwiGO1F/V4AyK96NGF/FB04FhOUDuWj8IKfABA=="], + "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], "schema-utils": ["schema-utils@4.3.2", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ=="], - "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "send": ["send@0.19.1", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg=="], @@ -1744,8 +1573,6 @@ "sf-symbols-typescript": ["sf-symbols-typescript@2.1.0", "", {}, "sha512-ezT7gu/SHTPIOEEoG6TF+O0m5eewl0ZDAO4AtdBi5HjsrUI6JdCG17+Q8+aKp0heM06wZKApRCn5olNbs0Wb/A=="], - "shallow-clone": ["shallow-clone@3.0.1", "", { "dependencies": { "kind-of": "^6.0.2" } }, "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA=="], - "shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -1754,8 +1581,6 @@ "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], - "shelljs": ["shelljs@0.8.5", "", { "dependencies": { "glob": "^7.0.0", "interpret": "^1.0.0", "rechoir": "^0.6.2" }, "bin": { "shjs": "bin/shjs" } }, "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow=="], - "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "simple-plist": ["simple-plist@1.3.1", "", { "dependencies": { "bplist-creator": "0.1.0", "bplist-parser": "0.3.1", "plist": "^3.0.5" } }, "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw=="], @@ -1838,8 +1663,6 @@ "tar": ["tar@7.5.1", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g=="], - "temp": ["temp@0.8.4", "", { "dependencies": { "rimraf": "~2.6.2" } }, "sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg=="], - "temp-dir": ["temp-dir@2.0.0", "", {}, "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg=="], "terminal-link": ["terminal-link@2.1.1", "", { "dependencies": { "ansi-escapes": "^4.2.1", "supports-hyperlinks": "^2.0.0" } }, "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ=="], @@ -1864,8 +1687,6 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], - "tough-cookie": ["tough-cookie@4.1.4", "", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag=="], "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], @@ -1880,8 +1701,6 @@ "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], - "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], - "uglify-es": ["uglify-es@3.3.9", "", { "dependencies": { "commander": "~2.13.0", "source-map": "~0.6.1" }, "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ=="], "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], @@ -2000,223 +1819,25 @@ "zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="], - "@amazon-devices/kepler-module-manifest-builder/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native": ["react-native@0.72.0", "", { "dependencies": { "@jest/create-cache-key-function": "^29.2.1", "@react-native-community/cli": "11.3.2", "@react-native-community/cli-platform-android": "11.3.2", "@react-native-community/cli-platform-ios": "11.3.2", "@react-native/assets-registry": "^0.72.0", "@react-native/codegen": "^0.72.6", "@react-native/gradle-plugin": "^0.72.10", "@react-native/js-polyfills": "^0.72.1", "@react-native/normalize-colors": "^0.72.0", "@react-native/virtualized-lists": "^0.72.5", "abort-controller": "^3.0.0", "anser": "^1.4.9", "base64-js": "^1.1.2", "deprecated-react-native-prop-types": "4.1.0", "event-target-shim": "^5.0.1", "flow-enums-runtime": "^0.0.5", "invariant": "^2.2.4", "jest-environment-node": "^29.2.1", "jsc-android": "^250231.0.0", "memoize-one": "^5.0.0", "metro-runtime": "0.76.5", "metro-source-map": "0.76.5", "mkdirp": "^0.5.1", "nullthrows": "^1.1.1", "pretty-format": "^26.5.2", "promise": "^8.3.0", "react-devtools-core": "^4.27.2", "react-refresh": "^0.4.0", "react-shallow-renderer": "^16.15.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.24.0-canary-efb381bbf-20230505", "stacktrace-parser": "^0.1.10", "use-sync-external-store": "^1.0.0", "whatwg-fetch": "^3.0.0", "ws": "^6.2.2", "yargs": "^17.6.2" }, "peerDependencies": { "react": "18.2.0" }, "bin": { "react-native": "cli.js" } }, "sha512-BP2qq5f05pl3zV+eYgW7nmjCV+3zLXs/Vm69xMTCLD34U2bvRhuWbuUhqpauw7fyk5c8+LrY9NKcnARCkqKXcw=="], - - "@amazon-devices/react-native-kepler/@babel/traverse": ["@babel/traverse@7.23.2", "", { "dependencies": { "@babel/code-frame": "^7.22.13", "@babel/generator": "^7.23.0", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "@babel/parser": "^7.23.0", "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" } }, "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli": ["@react-native-community/cli@11.4.1", "", { "dependencies": { "@react-native-community/cli-clean": "11.4.1", "@react-native-community/cli-config": "11.4.1", "@react-native-community/cli-debugger-ui": "11.4.1", "@react-native-community/cli-doctor": "11.4.1", "@react-native-community/cli-hermes": "11.4.1", "@react-native-community/cli-plugin-metro": "11.4.1", "@react-native-community/cli-server-api": "11.4.1", "@react-native-community/cli-tools": "11.4.1", "@react-native-community/cli-types": "11.4.1", "chalk": "^4.1.2", "commander": "^9.4.1", "execa": "^5.0.0", "find-up": "^4.1.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "prompts": "^2.4.0", "semver": "^7.5.2" }, "bin": { "react-native": "build/bin.js" } }, "sha512-NdAageVMtNhtvRsrq4NgJf5Ey2nA1CqmLvn7PhSawg+aIzMKmZuzWxGVwr9CoPGyjvNiqJlCWrLGR7NzOyi/sA=="], - - "@amazon-devices/react-native-kepler/@react-native/js-polyfills": ["@react-native/js-polyfills@0.72.1", "", {}, "sha512-cRPZh2rBswFnGt5X5EUEPs0r+pAsXxYsifv/fgy9ZLQokuT52bPH+9xjDR+7TafRua5CttGW83wP4TntRcWNDA=="], - - "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@babel/highlight/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], - "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/plugin-bugfix-safari-class-field-initializer-scope/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], - - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], - - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ=="], - - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/plugin-syntax-import-assertions/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-syntax-import-attributes/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-syntax-unicode-sets-regex/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-async-generator-functions/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-wrap-function": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og=="], - - "@babel/plugin-transform-async-generator-functions/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - "@babel/plugin-transform-block-scoped-functions/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg=="], - - "@babel/plugin-transform-dotall-regex/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-duplicate-keys/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg=="], - - "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-dynamic-import/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-explicit-resource-management/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-explicit-resource-management/@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg=="], - - "@babel/plugin-transform-exponentiation-operator/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-for-of/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], - - "@babel/plugin-transform-json-strings/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-logical-assignment-operators/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - "@babel/plugin-transform-member-expression-literals/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], - - "@babel/plugin-transform-modules-amd/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-modules-systemjs/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], - - "@babel/plugin-transform-modules-systemjs/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-modules-systemjs/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/plugin-transform-modules-systemjs/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], - - "@babel/plugin-transform-modules-umd/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-new-target/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-numeric-separator/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - "@babel/plugin-transform-object-super/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], "@babel/plugin-transform-object-super/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="], - "@babel/plugin-transform-optional-catch-binding/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - "@babel/plugin-transform-property-literals/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - "@babel/plugin-transform-regenerator/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg=="], - - "@babel/plugin-transform-regexp-modifiers/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-reserved-words/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-runtime/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/plugin-transform-typeof-symbol/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-unicode-escapes/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg=="], - - "@babel/plugin-transform-unicode-property-regex/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg=="], - - "@babel/plugin-transform-unicode-sets-regex/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/preset-env/@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], - - "@babel/preset-env/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], - - "@babel/preset-env/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/preset-env/@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], - - "@babel/preset-env/@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-remap-async-to-generator": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w=="], - - "@babel/preset-env/@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A=="], - - "@babel/preset-env/@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g=="], - - "@babel/preset-env/@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/template": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA=="], - - "@babel/preset-env/@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg=="], - - "@babel/preset-env/@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA=="], - - "@babel/preset-env/@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.29.7", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg=="], - - "@babel/preset-env/@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw=="], - - "@babel/preset-env/@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="], - - "@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ=="], - - "@babel/preset-env/@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg=="], - - "@babel/preset-env/@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.29.7", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-transform-destructuring": "^7.29.7", "@babel/plugin-transform-parameters": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A=="], - - "@babel/preset-env/@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ=="], - - "@babel/preset-env/@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA=="], - - "@babel/preset-env/@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg=="], - - "@babel/preset-env/@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ=="], - - "@babel/preset-env/@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA=="], - - "@babel/preset-env/@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA=="], - - "@babel/preset-env/@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA=="], - - "@babel/preset-env/babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.17", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-define-polyfill-provider": "^0.6.8", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w=="], - - "@babel/preset-env/babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.14.2", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.8", "core-js-compat": "^3.48.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g=="], - - "@babel/preset-env/babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.8", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.8" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg=="], - - "@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/preset-flow/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/preset-flow/@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], - - "@babel/preset-flow/@babel/plugin-transform-flow-strip-types": ["@babel/plugin-transform-flow-strip-types@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-flow": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ=="], - - "@babel/preset-modules/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/preset-modules/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@expo/cli/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="], "@expo/cli/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], "@expo/cli/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "@expo/cli/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + "@expo/cli/ora": ["ora@3.4.0", "", { "dependencies": { "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-spinners": "^2.0.0", "log-symbols": "^2.2.0", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1" } }, "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg=="], "@expo/cli/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -2270,8 +1891,6 @@ "@expo/metro/metro-runtime": ["metro-runtime@0.83.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-3Ag8ZS4IwafL/JUKlaeM6/CbkooY+WcVeqdNlBG0m4S0Qz0om3rdFdy1y6fYBpl6AwXJwWeMuXrvZdMuByTcRA=="], - "@expo/metro/metro-source-map": ["metro-source-map@0.83.1", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.1", "nullthrows": "^1.1.1", "ob1": "0.83.1", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-De7Vbeo96fFZ2cqmI0fWwVJbtHIwPZv++LYlWSwzTiCzxBDJORncN0LcT48Vi2UlQLzXJg+/CuTAcy7NBVh69A=="], - "@expo/metro-config/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="], "@expo/metro-config/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], @@ -2280,7 +1899,7 @@ "@expo/metro-config/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "@expo/prebuild-config/@react-native/normalize-colors": ["@react-native/normalize-colors@0.81.4", "", {}, "sha512-9nRRHO1H+tcFqjb9gAM105Urtgcanbta2tuqCVY0NATHeFPDEAB7gPyiLxCHKMi1NbhP6TH0kxgSWXKZl1cyRg=="], + "@expo/package-manager/ora": ["ora@3.4.0", "", { "dependencies": { "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-spinners": "^2.0.0", "log-symbols": "^2.2.0", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1" } }, "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg=="], "@expo/prebuild-config/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -2300,8 +1919,6 @@ "@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - "@jest/core/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - "@jest/core/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "@jest/reporters/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -2320,70 +1937,32 @@ "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@react-native-community/cli/@react-native-community/cli-tools": ["@react-native-community/cli-tools@11.3.2", "", { "dependencies": { "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "find-up": "^5.0.0", "mime": "^2.4.1", "node-fetch": "^2.6.0", "open": "^6.2.0", "ora": "^5.4.1", "semver": "^6.3.0", "shell-quote": "^1.7.3" } }, "sha512-rAnFPzRITeEhBLwC73ucvWsYjsGyotSOI4c+k8t9wUqcIk1Q+RFnuWozGc13msOPdESvBHt2MPJBwXrtKgKn1g=="], - - "@react-native-community/cli/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@react-native-community/cli-clean/@react-native-community/cli-tools": ["@react-native-community/cli-tools@11.3.2", "", { "dependencies": { "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "find-up": "^5.0.0", "mime": "^2.4.1", "node-fetch": "^2.6.0", "open": "^6.2.0", "ora": "^5.4.1", "semver": "^6.3.0", "shell-quote": "^1.7.3" } }, "sha512-rAnFPzRITeEhBLwC73ucvWsYjsGyotSOI4c+k8t9wUqcIk1Q+RFnuWozGc13msOPdESvBHt2MPJBwXrtKgKn1g=="], - - "@react-native-community/cli-config/@react-native-community/cli-tools": ["@react-native-community/cli-tools@11.3.2", "", { "dependencies": { "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "find-up": "^5.0.0", "mime": "^2.4.1", "node-fetch": "^2.6.0", "open": "^6.2.0", "ora": "^5.4.1", "semver": "^6.3.0", "shell-quote": "^1.7.3" } }, "sha512-rAnFPzRITeEhBLwC73ucvWsYjsGyotSOI4c+k8t9wUqcIk1Q+RFnuWozGc13msOPdESvBHt2MPJBwXrtKgKn1g=="], - "@react-native-community/cli-config/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "@react-native-community/cli-doctor/@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@11.3.2", "", { "dependencies": { "@react-native-community/cli-tools": "11.3.2", "chalk": "^4.1.2", "execa": "^5.0.0", "glob": "^7.1.3", "logkitty": "^0.7.1" } }, "sha512-NKxyBP0/gwL4/tNWrkevFSjeb7Dw2SByNfE9wFXBaAvZHxbxxJUjZOTOW3ueOXEpgOMU7IYYOiSOz2M10IRQ2A=="], - - "@react-native-community/cli-doctor/@react-native-community/cli-platform-ios": ["@react-native-community/cli-platform-ios@11.3.2", "", { "dependencies": { "@react-native-community/cli-tools": "11.3.2", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-xml-parser": "^4.0.12", "glob": "^7.1.3", "ora": "^5.4.1" } }, "sha512-XPrfsI7dNY3f9crsKDDRIss+GHYX/snuYfMrjg4ZBHpYB5JdLepO8FJ5bFz+/s9KXDm045ijo8QFcIf3XJR0YQ=="], - - "@react-native-community/cli-doctor/@react-native-community/cli-tools": ["@react-native-community/cli-tools@11.3.2", "", { "dependencies": { "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "find-up": "^5.0.0", "mime": "^2.4.1", "node-fetch": "^2.6.0", "open": "^6.2.0", "ora": "^5.4.1", "semver": "^6.3.0", "shell-quote": "^1.7.3" } }, "sha512-rAnFPzRITeEhBLwC73ucvWsYjsGyotSOI4c+k8t9wUqcIk1Q+RFnuWozGc13msOPdESvBHt2MPJBwXrtKgKn1g=="], - - "@react-native-community/cli-doctor/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - - "@react-native-community/cli-doctor/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@react-native-community/cli-hermes/@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@11.3.2", "", { "dependencies": { "@react-native-community/cli-tools": "11.3.2", "chalk": "^4.1.2", "execa": "^5.0.0", "glob": "^7.1.3", "logkitty": "^0.7.1" } }, "sha512-NKxyBP0/gwL4/tNWrkevFSjeb7Dw2SByNfE9wFXBaAvZHxbxxJUjZOTOW3ueOXEpgOMU7IYYOiSOz2M10IRQ2A=="], - - "@react-native-community/cli-hermes/@react-native-community/cli-tools": ["@react-native-community/cli-tools@11.3.2", "", { "dependencies": { "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "find-up": "^5.0.0", "mime": "^2.4.1", "node-fetch": "^2.6.0", "open": "^6.2.0", "ora": "^5.4.1", "semver": "^6.3.0", "shell-quote": "^1.7.3" } }, "sha512-rAnFPzRITeEhBLwC73ucvWsYjsGyotSOI4c+k8t9wUqcIk1Q+RFnuWozGc13msOPdESvBHt2MPJBwXrtKgKn1g=="], - "@react-native-community/cli-platform-android/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "@react-native-community/cli-platform-ios/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "@react-native-community/cli-platform-ios/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - - "@react-native-community/cli-plugin-metro/@react-native-community/cli-tools": ["@react-native-community/cli-tools@11.3.2", "", { "dependencies": { "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "find-up": "^5.0.0", "mime": "^2.4.1", "node-fetch": "^2.6.0", "open": "^6.2.0", "ora": "^5.4.1", "semver": "^6.3.0", "shell-quote": "^1.7.3" } }, "sha512-rAnFPzRITeEhBLwC73ucvWsYjsGyotSOI4c+k8t9wUqcIk1Q+RFnuWozGc13msOPdESvBHt2MPJBwXrtKgKn1g=="], - "@react-native-community/cli-plugin-metro/metro-config": ["metro-config@0.76.5", "", { "dependencies": { "cosmiconfig": "^5.0.5", "jest-validate": "^29.2.1", "metro": "0.76.5", "metro-cache": "0.76.5", "metro-core": "0.76.5", "metro-runtime": "0.76.5" } }, "sha512-SCMVIDOtm8s3H62E9z2IcY4Q9GVMqDurbiJS3PHrWgTZjwZFaL59lrW4W6DvzvFZHa9bbxKric5TFtwvVuyOCg=="], "@react-native-community/cli-plugin-metro/metro-react-native-babel-transformer": ["metro-react-native-babel-transformer@0.76.5", "", { "dependencies": { "@babel/core": "^7.20.0", "babel-preset-fbjs": "^3.4.0", "hermes-parser": "0.8.0", "metro-babel-transformer": "0.76.5", "metro-react-native-babel-preset": "0.76.5", "metro-source-map": "0.76.5", "nullthrows": "^1.1.1" } }, "sha512-7m2u7jQ1I2mwGm48Vrki5cNNSv4d2HegHMGmE5G2AAa6Pr2O3ajaX2yNoAKF8TCLO38/8pa9fZd0VWAlO/YMcA=="], - "@react-native-community/cli-server-api/@react-native-community/cli-tools": ["@react-native-community/cli-tools@11.3.2", "", { "dependencies": { "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "find-up": "^5.0.0", "mime": "^2.4.1", "node-fetch": "^2.6.0", "open": "^6.2.0", "ora": "^5.4.1", "semver": "^6.3.0", "shell-quote": "^1.7.3" } }, "sha512-rAnFPzRITeEhBLwC73ucvWsYjsGyotSOI4c+k8t9wUqcIk1Q+RFnuWozGc13msOPdESvBHt2MPJBwXrtKgKn1g=="], + "@react-native-community/cli-plugin-metro/metro-runtime": ["metro-runtime@0.76.5", "", { "dependencies": { "@babel/runtime": "^7.0.0", "react-refresh": "^0.4.0" } }, "sha512-1JAf9/v/NDHLhoTfiJ0n25G6dRkX7mjTkaMJ6UUXIyfIuSucoK5yAuOBx8OveNIekoLRjmyvSmyN5ojEeRmpvQ=="], + + "@react-native-community/cli-server-api/pretty-format": ["pretty-format@26.6.2", "", { "dependencies": { "@jest/types": "^26.6.2", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^17.0.1" } }, "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg=="], "@react-native-community/cli-server-api/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], "@react-native-community/cli-tools/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - "@react-native-community/cli-tools/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - - "@react-native-community/cli-tools/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - "@react-native/babel-plugin-codegen/@react-native/codegen": ["@react-native/codegen@0.81.4", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.29.1", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-LWTGUTzFu+qOQnvkzBP52B90Ym3stZT8IFCzzUrppz8Iwglg83FCtDZAR4yLHI29VY/x/+pkcWAMCl3739XHdw=="], - "@react-native/babel-preset/@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q=="], - - "@react-native/babel-preset/@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw=="], - - "@react-native/babel-preset/@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw=="], - - "@react-native/babel-preset/@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw=="], - - "@react-native/babel-preset/@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q=="], - - "@react-native/babel-preset/@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.28.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg=="], - "@react-native/babel-preset/react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], "@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "@react-native/codegen/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="], + "@react-native/community-cli-plugin/@react-native/dev-middleware": ["@react-native/dev-middleware@0.81.5", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.81.5", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^6.2.3" } }, "sha512-WfPfZzboYgo/TUtysuD5xyANzzfka8Ebni6RIb2wDxhb56ERi7qDrE4xGhtPsjCL4pQBXSVxyIlCy0d8I6EgGA=="], "@react-native/community-cli-plugin/metro": ["metro@0.83.1", "", { "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", "accepts": "^1.3.7", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.29.1", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.1", "metro-cache": "0.83.1", "metro-cache-key": "0.83.1", "metro-config": "0.83.1", "metro-core": "0.83.1", "metro-file-map": "0.83.1", "metro-resolver": "0.83.1", "metro-runtime": "0.83.1", "metro-source-map": "0.83.1", "metro-symbolicate": "0.83.1", "metro-transform-plugins": "0.83.1", "metro-transform-worker": "0.83.1", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-UGKepmTxoGD4HkQV8YWvpvwef7fUujNtTgG4Ygf7m/M0qjvb9VuDmAsEU+UdriRX7F61pnVK/opz89hjKlYTXA=="], @@ -2396,16 +1975,6 @@ "@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], - "@react-native/metro-config/@react-native/js-polyfills": ["@react-native/js-polyfills@0.72.1", "", {}, "sha512-cRPZh2rBswFnGt5X5EUEPs0r+pAsXxYsifv/fgy9ZLQokuT52bPH+9xjDR+7TafRua5CttGW83wP4TntRcWNDA=="], - - "@react-native/metro-config/metro-runtime": ["metro-runtime@0.76.9", "", { "dependencies": { "@babel/runtime": "^7.0.0", "react-refresh": "^0.4.0" } }, "sha512-/5vezDpGUtA0Fv6cJg0+i6wB+QeBbvLeaw9cTSG7L76liP0b91f8vOcYzGaUbHI8pznJCCTerxRzpQ8e3/NcDw=="], - - "@testing-library/jest-native/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - - "@testing-library/react-native/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - - "@types/jest/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - "@types/react-test-renderer/@types/react": ["@types/react@18.3.23", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w=="], "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], @@ -2414,20 +1983,12 @@ "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "babel-plugin-polyfill-corejs3/core-js-compat": ["core-js-compat@3.44.0", "", { "dependencies": { "browserslist": "^4.25.1" } }, "sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA=="], - "babel-plugin-syntax-hermes-parser/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="], - "babel-preset-current-node-syntax/@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww=="], - "babel-preset-expo/react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], "babel-preset-fbjs/@babel/plugin-transform-flow-strip-types": ["@babel/plugin-transform-flow-strip-types@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-flow": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ=="], - "babel-preset-fbjs/@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw=="], - "better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], "caller-callsite/callsites": ["callsites@2.0.0", "", {}, "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ=="], @@ -2436,22 +1997,16 @@ "chromium-edge-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - "chromium-edge-launcher/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], - "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "connect/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "core-js-compat/browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], - "cssstyle/cssom": ["cssom@0.3.8", "", {}, "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="], "data-urls/whatwg-url": ["whatwg-url@11.0.0", "", { "dependencies": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" } }, "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ=="], - "deprecated-react-native-prop-types/@react-native/normalize-colors": ["@react-native/normalize-colors@0.81.5", "", {}, "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g=="], - "domexception/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], "error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], @@ -2460,8 +2015,6 @@ "eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], - "expo/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - "expo/react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], "expo-build-properties/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -2474,8 +2027,6 @@ "expo-router/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], - "expo-system-ui/@react-native/normalize-colors": ["@react-native/normalize-colors@0.81.4", "", {}, "sha512-9nRRHO1H+tcFqjb9gAM105Urtgcanbta2tuqCVY0NATHeFPDEAB7gPyiLxCHKMi1NbhP6TH0kxgSWXKZl1cyRg=="], - "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "finalhandler/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], @@ -2484,8 +2035,6 @@ "finalhandler/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], - "find-cache-dir/pkg-dir": ["pkg-dir@3.0.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw=="], - "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "hermes-profile-transformer/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], @@ -2496,46 +2045,24 @@ "import-fresh/resolve-from": ["resolve-from@3.0.0", "", {}, "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw=="], - "istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "istanbul-lib-report/make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], - "istanbul-lib-source-maps/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "jest-circus/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - "jest-config/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "jest-config/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], - "jest-config/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - - "jest-diff/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - - "jest-each/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - "jest-haste-map/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], - "jest-leak-detector/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - - "jest-matcher-utils/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - - "jest-message-util/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - "jest-runner/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], "jest-runner/source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="], "jest-runtime/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "jest-snapshot/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - "jest-snapshot/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "jest-validate/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - "jest-watch-select-projects/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], "jest-watch-typeahead/ansi-escapes": ["ansi-escapes@6.2.1", "", {}, "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig=="], @@ -2548,8 +2075,6 @@ "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "jscodeshift/write-file-atomic": ["write-file-atomic@2.4.3", "", { "dependencies": { "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", "signal-exit": "^3.0.2" } }, "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ=="], - "jsdom/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], "jsdom/whatwg-url": ["whatwg-url@11.0.0", "", { "dependencies": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" } }, "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ=="], @@ -2558,11 +2083,9 @@ "lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "log-symbols/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], - "logkitty/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], - "make-dir/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + "make-dir/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], @@ -2582,6 +2105,12 @@ "metro/metro-react-native-babel-preset": ["metro-react-native-babel-preset@0.76.5", "", { "dependencies": { "@babel/core": "^7.20.0", "@babel/plugin-proposal-async-generator-functions": "^7.0.0", "@babel/plugin-proposal-class-properties": "^7.18.0", "@babel/plugin-proposal-export-default-from": "^7.0.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.0", "@babel/plugin-proposal-numeric-separator": "^7.0.0", "@babel/plugin-proposal-object-rest-spread": "^7.20.0", "@babel/plugin-proposal-optional-catch-binding": "^7.0.0", "@babel/plugin-proposal-optional-chaining": "^7.20.0", "@babel/plugin-syntax-dynamic-import": "^7.8.0", "@babel/plugin-syntax-export-default-from": "^7.0.0", "@babel/plugin-syntax-flow": "^7.18.0", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.0.0", "@babel/plugin-syntax-optional-chaining": "^7.0.0", "@babel/plugin-transform-arrow-functions": "^7.0.0", "@babel/plugin-transform-async-to-generator": "^7.20.0", "@babel/plugin-transform-block-scoping": "^7.0.0", "@babel/plugin-transform-classes": "^7.0.0", "@babel/plugin-transform-computed-properties": "^7.0.0", "@babel/plugin-transform-destructuring": "^7.20.0", "@babel/plugin-transform-flow-strip-types": "^7.20.0", "@babel/plugin-transform-function-name": "^7.0.0", "@babel/plugin-transform-literals": "^7.0.0", "@babel/plugin-transform-modules-commonjs": "^7.0.0", "@babel/plugin-transform-named-capturing-groups-regex": "^7.0.0", "@babel/plugin-transform-parameters": "^7.0.0", "@babel/plugin-transform-react-display-name": "^7.0.0", "@babel/plugin-transform-react-jsx": "^7.0.0", "@babel/plugin-transform-react-jsx-self": "^7.0.0", "@babel/plugin-transform-react-jsx-source": "^7.0.0", "@babel/plugin-transform-runtime": "^7.0.0", "@babel/plugin-transform-shorthand-properties": "^7.0.0", "@babel/plugin-transform-spread": "^7.0.0", "@babel/plugin-transform-sticky-regex": "^7.0.0", "@babel/plugin-transform-typescript": "^7.5.0", "@babel/plugin-transform-unicode-regex": "^7.0.0", "@babel/template": "^7.0.0", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.4.0" } }, "sha512-IlVKeTon5fef77rQ6WreSmrabmbc3dEsLwr/sL80fYjobjsD8FRCnOlbaJdgUf2SMJmSIoawgjh5Yeebv+gJzg=="], + "metro/metro-runtime": ["metro-runtime@0.76.5", "", { "dependencies": { "@babel/runtime": "^7.0.0", "react-refresh": "^0.4.0" } }, "sha512-1JAf9/v/NDHLhoTfiJ0n25G6dRkX7mjTkaMJ6UUXIyfIuSucoK5yAuOBx8OveNIekoLRjmyvSmyN5ojEeRmpvQ=="], + + "metro/metro-source-map": ["metro-source-map@0.76.5", "", { "dependencies": { "@babel/traverse": "^7.20.0", "@babel/types": "^7.20.0", "invariant": "^2.2.4", "metro-symbolicate": "0.76.5", "nullthrows": "^1.1.1", "ob1": "0.76.5", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-1EhYPcoftONlvnOzgos7daE8hsJKOgSN3nD3Xf/yaY1F0aLeGeuWfpiNLLeFDNyUhfObHSuNxNhDQF/x1GFEbw=="], + + "metro/metro-symbolicate": ["metro-symbolicate@0.76.5", "", { "dependencies": { "invariant": "^2.2.4", "metro-source-map": "0.76.5", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "through2": "^2.0.1", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-7iftzh6G6HO4UDBmjsi2Yu4d6IkApv6Kg+jmBvkTjCXr8HwnKKum89gMg/FRMix+Rhhut0dnMpz6mAbtKTU9JQ=="], + "metro/metro-transform-plugins": ["metro-transform-plugins@0.76.5", "", { "dependencies": { "@babel/core": "^7.20.0", "@babel/generator": "^7.20.0", "@babel/template": "^7.0.0", "@babel/traverse": "^7.20.0", "nullthrows": "^1.1.1" } }, "sha512-7pJ24aRuvzdQYpX/eOyodr4fnwVJP5ArNLBE1d0DOU9sQxsGplOORDTGAqw2L01+UgaSJiiwEoFMw7Z91HAS+Q=="], "metro/metro-transform-worker": ["metro-transform-worker@0.76.5", "", { "dependencies": { "@babel/core": "^7.20.0", "@babel/generator": "^7.20.0", "@babel/parser": "^7.20.0", "@babel/types": "^7.20.0", "babel-preset-fbjs": "^3.4.0", "metro": "0.76.5", "metro-babel-transformer": "0.76.5", "metro-cache": "0.76.5", "metro-cache-key": "0.76.5", "metro-source-map": "0.76.5", "metro-transform-plugins": "0.76.5", "nullthrows": "^1.1.1" } }, "sha512-xN6Kb06o9u5A7M1bbl7oPfQFmt4Kmi3CMXp5j9OcK37AFc+u6YXH8x/6e9b3Cq50rlBYuCXDOOYAWI5/tYNt2w=="], @@ -2590,51 +2119,33 @@ "metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], - "metro-babel-transformer/flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - "metro-babel-transformer/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="], "metro-cache/metro-core": ["metro-core@0.76.9", "", { "dependencies": { "lodash.throttle": "^4.1.1", "metro-resolver": "0.76.9" } }, "sha512-DSeEr43Wrd5Q7ySfRzYzDwfV89g2OZTQDf1s3exOcLjE5fb7awoLOkA2h46ZzN8NcmbbM0cuJy6hOwF073/yRQ=="], - "metro-cache-key/flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - "metro-config/metro": ["metro@0.76.9", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "@babel/core": "^7.20.0", "@babel/generator": "^7.20.0", "@babel/parser": "^7.20.0", "@babel/template": "^7.0.0", "@babel/traverse": "^7.20.0", "@babel/types": "^7.20.0", "accepts": "^1.3.7", "async": "^3.2.2", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^2.2.0", "denodeify": "^1.2.1", "error-stack-parser": "^2.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.12.0", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^27.2.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.76.9", "metro-cache": "0.76.9", "metro-cache-key": "0.76.9", "metro-config": "0.76.9", "metro-core": "0.76.9", "metro-file-map": "0.76.9", "metro-inspector-proxy": "0.76.9", "metro-minify-uglify": "0.76.9", "metro-react-native-babel-preset": "0.76.9", "metro-resolver": "0.76.9", "metro-runtime": "0.76.9", "metro-source-map": "0.76.9", "metro-symbolicate": "0.76.9", "metro-transform-plugins": "0.76.9", "metro-transform-worker": "0.76.9", "mime-types": "^2.1.27", "node-fetch": "^2.2.0", "nullthrows": "^1.1.1", "rimraf": "^3.0.2", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "strip-ansi": "^6.0.0", "throat": "^5.0.0", "ws": "^7.5.1", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-gcjcfs0l5qIPg0lc5P7pj0x7vPJ97tan+OnEjiYLbKjR1D7Oa78CE93YUPyymUPH6q7VzlzMm1UjT35waEkZUw=="], "metro-config/metro-core": ["metro-core@0.76.9", "", { "dependencies": { "lodash.throttle": "^4.1.1", "metro-resolver": "0.76.9" } }, "sha512-DSeEr43Wrd5Q7ySfRzYzDwfV89g2OZTQDf1s3exOcLjE5fb7awoLOkA2h46ZzN8NcmbbM0cuJy6hOwF073/yRQ=="], - "metro-config/metro-runtime": ["metro-runtime@0.76.9", "", { "dependencies": { "@babel/runtime": "^7.0.0", "react-refresh": "^0.4.0" } }, "sha512-/5vezDpGUtA0Fv6cJg0+i6wB+QeBbvLeaw9cTSG7L76liP0b91f8vOcYzGaUbHI8pznJCCTerxRzpQ8e3/NcDw=="], - - "metro-file-map/flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - "metro-file-map/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], "metro-inspector-proxy/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "metro-inspector-proxy/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], - "metro-transform-plugins/flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - - "metro-transform-worker/flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - "metro-transform-worker/metro": ["metro@0.83.1", "", { "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", "accepts": "^1.3.7", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.29.1", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.1", "metro-cache": "0.83.1", "metro-cache-key": "0.83.1", "metro-config": "0.83.1", "metro-core": "0.83.1", "metro-file-map": "0.83.1", "metro-resolver": "0.83.1", "metro-runtime": "0.83.1", "metro-source-map": "0.83.1", "metro-symbolicate": "0.83.1", "metro-transform-plugins": "0.83.1", "metro-transform-worker": "0.83.1", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-UGKepmTxoGD4HkQV8YWvpvwef7fUujNtTgG4Ygf7m/M0qjvb9VuDmAsEU+UdriRX7F61pnVK/opz89hjKlYTXA=="], "metro-transform-worker/metro-cache": ["metro-cache@0.83.1", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.1" } }, "sha512-7N/Ad1PHa1YMWDNiyynTPq34Op2qIE68NWryGEQ4TSE3Zy6a8GpsYnEEZE4Qi6aHgsE+yZHKkRczeBgxhnFIxQ=="], "metro-transform-worker/metro-minify-terser": ["metro-minify-terser@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-kmooOxXLvKVxkh80IVSYO4weBdJDhCpg5NSPkjzzAnPJP43u6+usGXobkTWxxrAlq900bhzqKek4pBsUchlX6A=="], - "metro-transform-worker/metro-source-map": ["metro-source-map@0.83.1", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.1", "nullthrows": "^1.1.1", "ob1": "0.83.1", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-De7Vbeo96fFZ2cqmI0fWwVJbtHIwPZv++LYlWSwzTiCzxBDJORncN0LcT48Vi2UlQLzXJg+/CuTAcy7NBVh69A=="], - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "minizlib/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "mlly/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - - "node-dir/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "npm-package-arg/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - "ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + "ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], @@ -2644,11 +2155,9 @@ "pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], - "pretty-format/@jest/types": ["@jest/types@26.6.2", "", { "dependencies": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ=="], + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], - - "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], @@ -2656,52 +2165,28 @@ "react-dom/scheduler": ["scheduler@0.25.0-rc-6230622a1a-20240610", "", {}, "sha512-GTIQdJXthps5mgkIFo7yAq03M0QQYTfN8z+GrnMC/SCKFSuyFP5tk2BMaaWUsVy4u4r+dTLdiXH8JEivVls0Bw=="], - "react-native/@react-native/assets-registry": ["@react-native/assets-registry@0.81.5", "", {}, "sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w=="], - - "react-native/@react-native/codegen": ["@react-native/codegen@0.81.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.29.1", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-a2TDA03Up8lpSa9sh5VRGCQDXgCTOyDOFH+aqyinxp1HChG8uk89/G+nkJ9FPd0rqgi25eCTR16TWdS3b+fA6g=="], - - "react-native/@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.81.5", "", {}, "sha512-hORRlNBj+ReNMLo9jme3yQ6JQf4GZpVEBLxmTXGGlIL78MAezDZr5/uq9dwElSbcGmLEgeiax6e174Fie6qPLg=="], - "react-native/@react-native/js-polyfills": ["@react-native/js-polyfills@0.81.5", "", {}, "sha512-fB7M1CMOCIUudTRuj7kzxIBTVw2KXnsgbQ6+4cbqSxo8NmRRhA0Ul4ZUzZj3rFd3VznTL4Brmocv1oiN0bWZ8w=="], "react-native/@react-native/normalize-colors": ["@react-native/normalize-colors@0.81.5", "", {}, "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g=="], "react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - "react-native/flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - "react-native/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "react-native/metro-runtime": ["metro-runtime@0.83.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-3Ag8ZS4IwafL/JUKlaeM6/CbkooY+WcVeqdNlBG0m4S0Qz0om3rdFdy1y6fYBpl6AwXJwWeMuXrvZdMuByTcRA=="], - "react-native/metro-source-map": ["metro-source-map@0.83.1", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.1", "nullthrows": "^1.1.1", "ob1": "0.83.1", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-De7Vbeo96fFZ2cqmI0fWwVJbtHIwPZv++LYlWSwzTiCzxBDJORncN0LcT48Vi2UlQLzXJg+/CuTAcy7NBVh69A=="], - - "react-native/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - - "react-native/react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="], - "react-native/react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], - "react-native/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - "react-native/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "react-native-reanimated/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "react-native-worklets/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - "react-shallow-renderer/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "react-test-renderer/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - - "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "regjsparser/jsesc": ["jsesc@3.0.2", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="], "requireg/resolve": ["resolve@1.7.1", "", { "dependencies": { "path-parse": "^1.0.5" } }, "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw=="], - "restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="], - "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -2710,8 +2195,6 @@ "serve-static/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], - "shelljs/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "simple-plist/bplist-parser": ["bplist-parser@0.3.1", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA=="], "slice-ansi/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], @@ -2742,8 +2225,6 @@ "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - "temp/rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="], - "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -2766,209 +2247,25 @@ "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@11.3.2", "", { "dependencies": { "@react-native-community/cli-tools": "11.3.2", "chalk": "^4.1.2", "execa": "^5.0.0", "glob": "^7.1.3", "logkitty": "^0.7.1" } }, "sha512-NKxyBP0/gwL4/tNWrkevFSjeb7Dw2SByNfE9wFXBaAvZHxbxxJUjZOTOW3ueOXEpgOMU7IYYOiSOz2M10IRQ2A=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-ios": ["@react-native-community/cli-platform-ios@11.3.2", "", { "dependencies": { "@react-native-community/cli-tools": "11.3.2", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-xml-parser": "^4.0.12", "glob": "^7.1.3", "ora": "^5.4.1" } }, "sha512-XPrfsI7dNY3f9crsKDDRIss+GHYX/snuYfMrjg4ZBHpYB5JdLepO8FJ5bFz+/s9KXDm045ijo8QFcIf3XJR0YQ=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native/js-polyfills": ["@react-native/js-polyfills@0.72.1", "", {}, "sha512-cRPZh2rBswFnGt5X5EUEPs0r+pAsXxYsifv/fgy9ZLQokuT52bPH+9xjDR+7TafRua5CttGW83wP4TntRcWNDA=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-clean": ["@react-native-community/cli-clean@11.4.1", "", { "dependencies": { "@react-native-community/cli-tools": "11.4.1", "chalk": "^4.1.2", "execa": "^5.0.0", "prompts": "^2.4.0" } }, "sha512-cwUbY3c70oBGv3FvQJWe2Qkq6m1+/dcEBonMDTYyH6i+6OrkzI4RkIGpWmbG1IS5JfE9ISUZkNL3946sxyWNkw=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-config": ["@react-native-community/cli-config@11.4.1", "", { "dependencies": { "@react-native-community/cli-tools": "11.4.1", "chalk": "^4.1.2", "cosmiconfig": "^5.1.0", "deepmerge": "^4.3.0", "glob": "^7.1.3", "joi": "^17.2.1" } }, "sha512-sLdv1HFVqu5xNpeaR1+std0t7FFZaobpmpR0lFCOzKV7H/l611qS2Vo8zssmMK+oQbCs5JsX3SFPciODeIlaWA=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-debugger-ui": ["@react-native-community/cli-debugger-ui@11.4.1", "", { "dependencies": { "serve-static": "^1.13.1" } }, "sha512-+pgIjGNW5TrJF37XG3djIOzP+WNoPp67to/ggDhrshuYgpymfb9XpDVsURJugy0Sy3RViqb83kQNK765QzTIvw=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-doctor": ["@react-native-community/cli-doctor@11.4.1", "", { "dependencies": { "@react-native-community/cli-config": "11.4.1", "@react-native-community/cli-platform-android": "11.4.1", "@react-native-community/cli-platform-ios": "11.4.1", "@react-native-community/cli-tools": "11.4.1", "chalk": "^4.1.2", "command-exists": "^1.2.8", "envinfo": "^7.7.2", "execa": "^5.0.0", "hermes-profile-transformer": "^0.0.6", "node-stream-zip": "^1.9.1", "ora": "^5.4.1", "prompts": "^2.4.0", "semver": "^7.5.2", "strip-ansi": "^5.2.0", "sudo-prompt": "^9.0.0", "wcwidth": "^1.0.1", "yaml": "^2.2.1" } }, "sha512-O6oPiRsl8pdkcyNktpzvJAXUqdocoY4jh7Tt7wA69B1JKCJA7aPCecwJgpUZb63ZYoxOtRtYM3BYQKzRMLIuUw=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-hermes": ["@react-native-community/cli-hermes@11.4.1", "", { "dependencies": { "@react-native-community/cli-platform-android": "11.4.1", "@react-native-community/cli-tools": "11.4.1", "chalk": "^4.1.2", "hermes-profile-transformer": "^0.0.6" } }, "sha512-1VAjwcmv+i9BJTMMVn5Grw7AcgURhTyfHVghJ1YgBE2euEJxPuqPKSxP54wBOQKnWUwsuDQAtQf+jPJoCxJSSA=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro": ["@react-native-community/cli-plugin-metro@11.4.1", "", { "dependencies": { "@react-native-community/cli-server-api": "11.4.1", "@react-native-community/cli-tools": "11.4.1", "chalk": "^4.1.2", "execa": "^5.0.0", "metro": "^0.76.9", "metro-config": "^0.76.9", "metro-core": "^0.76.9", "metro-react-native-babel-transformer": "^0.76.9", "metro-resolver": "^0.76.9", "metro-runtime": "^0.76.9", "readline": "^1.3.0" } }, "sha512-JxbIqknYcQ5Z4rWROtu5LNakLfMiKoWcMoPqIrBLrV5ILm1XUJj1/8fATCcotZqV3yzB3SCJ3RrhKx7dQ3YELw=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-server-api": ["@react-native-community/cli-server-api@11.4.1", "", { "dependencies": { "@react-native-community/cli-debugger-ui": "11.4.1", "@react-native-community/cli-tools": "11.4.1", "compression": "^1.7.1", "connect": "^3.6.5", "errorhandler": "^1.5.1", "nocache": "^3.0.1", "pretty-format": "^26.6.2", "serve-static": "^1.13.1", "ws": "^7.5.1" } }, "sha512-isxXE8X5x+C4kN90yilD08jaLWD34hfqTfn/Xbl1u/igtdTsCaQGvWe9eaFamrpWFWTpVtj6k+vYfy8AtYSiKA=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-types": ["@react-native-community/cli-types@11.4.1", "", { "dependencies": { "joi": "^17.2.1" } }, "sha512-B3q9A5BCneLDSoK/iSJ06MNyBn1qTxjdJeOgeS3MiCxgJpPcxyn/Yrc6+h0Cu9T9sgWj/dmectQPYWxtZeo5VA=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - "@babel/highlight/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], "@babel/highlight/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], "@babel/highlight/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], - "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/@babel/helper-skip-transparent-expression-wrappers/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], - - "@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw=="], - - "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], - - "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], - - "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/plugin-transform-explicit-resource-management/@babel/plugin-transform-destructuring/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], - - "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/plugin-transform-modules-systemjs/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], - - "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], - - "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], - - "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], - - "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], - - "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-wrap-function": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/preset-env/@babel/plugin-transform-computed-properties/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/preset-env/@babel/plugin-transform-destructuring/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/preset-env/@babel/plugin-transform-function-name/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], - - "@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg=="], - - "@babel/preset-env/@babel/plugin-transform-object-rest-spread/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="], - - "@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], - - "@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg=="], - - "@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.8", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "debug": "^4.4.3", "lodash.debounce": "^4.0.8", "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA=="], - - "@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.8", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "debug": "^4.4.3", "lodash.debounce": "^4.0.8", "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA=="], - - "@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.8", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "debug": "^4.4.3", "lodash.debounce": "^4.0.8", "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA=="], - - "@babel/preset-flow/@babel/plugin-transform-flow-strip-types/@babel/plugin-syntax-flow": ["@babel/plugin-syntax-flow@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ=="], - - "@babel/preset-modules/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-modules/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - "@expo/cli/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "@expo/cli/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "@expo/cli/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], - "@expo/cli/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "@expo/cli/ora/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], + + "@expo/cli/ora/log-symbols": ["log-symbols@2.2.0", "", { "dependencies": { "chalk": "^2.0.1" } }, "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg=="], "@expo/config-plugins/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -2990,33 +2287,19 @@ "@expo/metro/metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], - "@expo/metro/metro/flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - "@expo/metro/metro/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="], "@expo/metro/metro/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], - "@expo/metro/metro/metro-symbolicate": ["metro-symbolicate@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.1", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-wPxYkONlq/Sv8Ji7vHEx5OzFouXAMQJjpcPW41ySKMLP/Ir18SsiJK2h4YkdKpYrTS1+0xf8oqF6nxCsT3uWtg=="], - "@expo/metro/metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], - "@expo/metro/metro-cache/flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - "@expo/metro/metro-cache/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - "@expo/metro/metro-config/flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - - "@expo/metro/metro-core/flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - - "@expo/metro/metro-resolver/flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - - "@expo/metro/metro-runtime/flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - - "@expo/metro/metro-source-map/flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], + "@expo/package-manager/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], - "@expo/metro/metro-source-map/metro-symbolicate": ["metro-symbolicate@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.1", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-wPxYkONlq/Sv8Ji7vHEx5OzFouXAMQJjpcPW41ySKMLP/Ir18SsiJK2h4YkdKpYrTS1+0xf8oqF6nxCsT3uWtg=="], + "@expo/package-manager/ora/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], - "@expo/metro/metro-source-map/ob1": ["ob1@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-ngwqewtdUzFyycomdbdIhFLjePPSOt1awKMUXQ0L7iLHgWEPF3DsCerblzjzfAUHaXuvE9ccJymWQ/4PNNqvnQ=="], + "@expo/package-manager/ora/log-symbols": ["log-symbols@2.2.0", "", { "dependencies": { "chalk": "^2.0.1" } }, "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg=="], "@expo/xcpretty/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], @@ -3028,10 +2311,6 @@ "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - "@jest/core/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "@jest/core/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "@jest/reporters/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "@jest/reporters/istanbul-lib-instrument/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -3040,56 +2319,12 @@ "@jest/reporters/string-length/char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], - "@react-native-community/cli-clean/@react-native-community/cli-tools/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "@react-native-community/cli-clean/@react-native-community/cli-tools/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - - "@react-native-community/cli-clean/@react-native-community/cli-tools/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@react-native-community/cli-config/@react-native-community/cli-tools/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "@react-native-community/cli-config/@react-native-community/cli-tools/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - - "@react-native-community/cli-config/@react-native-community/cli-tools/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@react-native-community/cli-config/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "@react-native-community/cli-doctor/@react-native-community/cli-platform-android/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - - "@react-native-community/cli-doctor/@react-native-community/cli-platform-ios/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - - "@react-native-community/cli-doctor/@react-native-community/cli-tools/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "@react-native-community/cli-doctor/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - - "@react-native-community/cli-doctor/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - - "@react-native-community/cli-doctor/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@react-native-community/cli-hermes/@react-native-community/cli-platform-android/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - - "@react-native-community/cli-hermes/@react-native-community/cli-tools/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "@react-native-community/cli-hermes/@react-native-community/cli-tools/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - - "@react-native-community/cli-hermes/@react-native-community/cli-tools/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@react-native-community/cli-platform-android/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "@react-native-community/cli-platform-ios/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "@react-native-community/cli-platform-ios/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - - "@react-native-community/cli-platform-ios/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - - "@react-native-community/cli-platform-ios/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@react-native-community/cli-plugin-metro/@react-native-community/cli-tools/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "@react-native-community/cli-plugin-metro/@react-native-community/cli-tools/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - - "@react-native-community/cli-plugin-metro/@react-native-community/cli-tools/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@react-native-community/cli-plugin-metro/metro-config/metro-cache": ["metro-cache@0.76.5", "", { "dependencies": { "metro-core": "0.76.5", "rimraf": "^3.0.2" } }, "sha512-8XalhoMNWDK6bi41oqxIpecTYRt4WsmtoHdqshgJIYshJ6qov0NuDw0pOfnS8rgMNHxPpuWyXc7NyKERqVRzaw=="], "@react-native-community/cli-plugin-metro/metro-react-native-babel-transformer/hermes-parser": ["hermes-parser@0.8.0", "", { "dependencies": { "hermes-estree": "0.8.0" } }, "sha512-yZKalg1fTYG5eOiToLUaw69rQfZq/fi+/NtEXRU7N87K/XobNRhRWorh80oSge2lWUiZfTgUvRJH+XgZWrhoqA=="], @@ -3098,38 +2333,28 @@ "@react-native-community/cli-plugin-metro/metro-react-native-babel-transformer/metro-react-native-babel-preset": ["metro-react-native-babel-preset@0.76.5", "", { "dependencies": { "@babel/core": "^7.20.0", "@babel/plugin-proposal-async-generator-functions": "^7.0.0", "@babel/plugin-proposal-class-properties": "^7.18.0", "@babel/plugin-proposal-export-default-from": "^7.0.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.0", "@babel/plugin-proposal-numeric-separator": "^7.0.0", "@babel/plugin-proposal-object-rest-spread": "^7.20.0", "@babel/plugin-proposal-optional-catch-binding": "^7.0.0", "@babel/plugin-proposal-optional-chaining": "^7.20.0", "@babel/plugin-syntax-dynamic-import": "^7.8.0", "@babel/plugin-syntax-export-default-from": "^7.0.0", "@babel/plugin-syntax-flow": "^7.18.0", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.0.0", "@babel/plugin-syntax-optional-chaining": "^7.0.0", "@babel/plugin-transform-arrow-functions": "^7.0.0", "@babel/plugin-transform-async-to-generator": "^7.20.0", "@babel/plugin-transform-block-scoping": "^7.0.0", "@babel/plugin-transform-classes": "^7.0.0", "@babel/plugin-transform-computed-properties": "^7.0.0", "@babel/plugin-transform-destructuring": "^7.20.0", "@babel/plugin-transform-flow-strip-types": "^7.20.0", "@babel/plugin-transform-function-name": "^7.0.0", "@babel/plugin-transform-literals": "^7.0.0", "@babel/plugin-transform-modules-commonjs": "^7.0.0", "@babel/plugin-transform-named-capturing-groups-regex": "^7.0.0", "@babel/plugin-transform-parameters": "^7.0.0", "@babel/plugin-transform-react-display-name": "^7.0.0", "@babel/plugin-transform-react-jsx": "^7.0.0", "@babel/plugin-transform-react-jsx-self": "^7.0.0", "@babel/plugin-transform-react-jsx-source": "^7.0.0", "@babel/plugin-transform-runtime": "^7.0.0", "@babel/plugin-transform-shorthand-properties": "^7.0.0", "@babel/plugin-transform-spread": "^7.0.0", "@babel/plugin-transform-sticky-regex": "^7.0.0", "@babel/plugin-transform-typescript": "^7.5.0", "@babel/plugin-transform-unicode-regex": "^7.0.0", "@babel/template": "^7.0.0", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.4.0" } }, "sha512-IlVKeTon5fef77rQ6WreSmrabmbc3dEsLwr/sL80fYjobjsD8FRCnOlbaJdgUf2SMJmSIoawgjh5Yeebv+gJzg=="], - "@react-native-community/cli-server-api/@react-native-community/cli-tools/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + "@react-native-community/cli-plugin-metro/metro-react-native-babel-transformer/metro-source-map": ["metro-source-map@0.76.5", "", { "dependencies": { "@babel/traverse": "^7.20.0", "@babel/types": "^7.20.0", "invariant": "^2.2.4", "metro-symbolicate": "0.76.5", "nullthrows": "^1.1.1", "ob1": "0.76.5", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-1EhYPcoftONlvnOzgos7daE8hsJKOgSN3nD3Xf/yaY1F0aLeGeuWfpiNLLeFDNyUhfObHSuNxNhDQF/x1GFEbw=="], - "@react-native-community/cli-server-api/@react-native-community/cli-tools/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], + "@react-native-community/cli-server-api/pretty-format/@jest/types": ["@jest/types@26.6.2", "", { "dependencies": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ=="], - "@react-native-community/cli-server-api/@react-native-community/cli-tools/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@react-native-community/cli-server-api/pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "@react-native-community/cli-tools/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - "@react-native-community/cli-tools/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - - "@react-native-community/cli-tools/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - - "@react-native-community/cli-tools/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@react-native-community/cli/@react-native-community/cli-tools/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "@react-native-community/cli/@react-native-community/cli-tools/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - "@react-native/babel-plugin-codegen/@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "@react-native/babel-plugin-codegen/@react-native/codegen/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="], "@react-native/codegen/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "@react-native/codegen/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="], + "@react-native/community-cli-plugin/@react-native/dev-middleware/@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.81.5", "", {}, "sha512-bnd9FSdWKx2ncklOetCgrlwqSGhMHP2zOxObJbOWXoj7GHEmih4MKarBo5/a8gX8EfA1EwRATdfNBQ81DY+h+w=="], "@react-native/community-cli-plugin/@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], "@react-native/community-cli-plugin/metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], - "@react-native/community-cli-plugin/metro/flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - "@react-native/community-cli-plugin/metro/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="], "@react-native/community-cli-plugin/metro/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], @@ -3140,36 +2365,16 @@ "@react-native/community-cli-plugin/metro/metro-runtime": ["metro-runtime@0.83.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-3Ag8ZS4IwafL/JUKlaeM6/CbkooY+WcVeqdNlBG0m4S0Qz0om3rdFdy1y6fYBpl6AwXJwWeMuXrvZdMuByTcRA=="], - "@react-native/community-cli-plugin/metro/metro-source-map": ["metro-source-map@0.83.1", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.1", "nullthrows": "^1.1.1", "ob1": "0.83.1", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-De7Vbeo96fFZ2cqmI0fWwVJbtHIwPZv++LYlWSwzTiCzxBDJORncN0LcT48Vi2UlQLzXJg+/CuTAcy7NBVh69A=="], - - "@react-native/community-cli-plugin/metro/metro-symbolicate": ["metro-symbolicate@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.1", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-wPxYkONlq/Sv8Ji7vHEx5OzFouXAMQJjpcPW41ySKMLP/Ir18SsiJK2h4YkdKpYrTS1+0xf8oqF6nxCsT3uWtg=="], - "@react-native/community-cli-plugin/metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], - "@react-native/community-cli-plugin/metro-config/flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - "@react-native/community-cli-plugin/metro-config/metro-cache": ["metro-cache@0.83.1", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.1" } }, "sha512-7N/Ad1PHa1YMWDNiyynTPq34Op2qIE68NWryGEQ4TSE3Zy6a8GpsYnEEZE4Qi6aHgsE+yZHKkRczeBgxhnFIxQ=="], "@react-native/community-cli-plugin/metro-config/metro-runtime": ["metro-runtime@0.83.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-3Ag8ZS4IwafL/JUKlaeM6/CbkooY+WcVeqdNlBG0m4S0Qz0om3rdFdy1y6fYBpl6AwXJwWeMuXrvZdMuByTcRA=="], - "@react-native/community-cli-plugin/metro-core/flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - "@react-native/community-cli-plugin/metro-core/metro-resolver": ["metro-resolver@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-t8j46kiILAqqFS5RNa+xpQyVjULxRxlvMidqUswPEk5nQVNdlJslqizDm/Et3v/JKwOtQGkYAQCHxP1zGStR/g=="], "@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - "@testing-library/jest-native/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "@testing-library/jest-native/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "@testing-library/react-native/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "@testing-library/react-native/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "@types/jest/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "@types/jest/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="], "babel-preset-fbjs/@babel/plugin-transform-flow-strip-types/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], @@ -3182,14 +2387,6 @@ "connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "core-js-compat/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001797", "", {}, "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w=="], - - "core-js-compat/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.369", "", {}, "sha512-XM22K9FNaaCOvMMrBn1caIc8v0g6+pKt660ZbfQqUZvfil0hEzr8ZoiY7VcSLGM3L/x3rz5PqZrk+bKOOmVM9w=="], - - "core-js-compat/browserslist/node-releases": ["node-releases@2.0.47", "", {}, "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og=="], - - "core-js-compat/browserslist/update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - "data-urls/whatwg-url/tr46": ["tr46@3.0.0", "", { "dependencies": { "punycode": "^2.1.1" } }, "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA=="], "data-urls/whatwg-url/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], @@ -3198,64 +2395,18 @@ "expo-modules-autolinking/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "expo-router/@expo/metro-runtime/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - - "expo/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "expo/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "find-cache-dir/pkg-dir/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], - - "istanbul-lib-report/make-dir/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "jest-circus/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "jest-circus/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "jest-config/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "jest-config/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "jest-config/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "jest-diff/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "jest-diff/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "jest-each/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "jest-each/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "jest-haste-map/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "jest-leak-detector/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "jest-leak-detector/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "jest-matcher-utils/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "jest-matcher-utils/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "jest-message-util/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "jest-message-util/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "jest-runner/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "jest-runner/source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "jest-runtime/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "jest-snapshot/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "jest-snapshot/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "jest-validate/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "jest-validate/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "jest-watch-typeahead/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "jest-watcher/string-length/char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], @@ -3266,12 +2417,6 @@ "lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "log-symbols/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], - - "log-symbols/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - - "log-symbols/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], - "logkitty/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], "logkitty/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], @@ -3330,18 +2475,12 @@ "metro-transform-worker/metro/metro-runtime": ["metro-runtime@0.83.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-3Ag8ZS4IwafL/JUKlaeM6/CbkooY+WcVeqdNlBG0m4S0Qz0om3rdFdy1y6fYBpl6AwXJwWeMuXrvZdMuByTcRA=="], - "metro-transform-worker/metro/metro-symbolicate": ["metro-symbolicate@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.1", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-wPxYkONlq/Sv8Ji7vHEx5OzFouXAMQJjpcPW41ySKMLP/Ir18SsiJK2h4YkdKpYrTS1+0xf8oqF6nxCsT3uWtg=="], - "metro-transform-worker/metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], "metro-transform-worker/metro-cache/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "metro-transform-worker/metro-cache/metro-core": ["metro-core@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.1" } }, "sha512-uVL1eAJcMFd2o2Q7dsbpg8COaxjZBBGaXqO2OHnivpCdfanraVL8dPmY6It9ZeqWLOihUKZ2yHW4b6soVCzH/Q=="], - "metro-transform-worker/metro-source-map/metro-symbolicate": ["metro-symbolicate@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.1", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-wPxYkONlq/Sv8Ji7vHEx5OzFouXAMQJjpcPW41ySKMLP/Ir18SsiJK2h4YkdKpYrTS1+0xf8oqF6nxCsT3uWtg=="], - - "metro-transform-worker/metro-source-map/ob1": ["ob1@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-ngwqewtdUzFyycomdbdIhFLjePPSOt1awKMUXQ0L7iLHgWEPF3DsCerblzjzfAUHaXuvE9ccJymWQ/4PNNqvnQ=="], - "metro/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "metro/hermes-parser/hermes-estree": ["hermes-estree@0.8.0", "", {}, "sha512-W6JDAOLZ5pMPMjEiQGLCXSSV7pIBEgRR5zGkxgmzGSXHOxqV5dC/M1Zevqpbm9TZDE5tu358qZf8Vkzmsc+u7Q=="], @@ -3352,34 +2491,12 @@ "metro/metro-react-native-babel-preset/@babel/plugin-transform-flow-strip-types": ["@babel/plugin-transform-flow-strip-types@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-flow": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ=="], - "node-dir/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], - - "ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - - "ora/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + "metro/metro-source-map/ob1": ["ob1@0.76.5", "", {}, "sha512-HoxZXMXNuY/eIXGoX7gx1C4O3eB4kJJMola6KoFaMm7PGGg39+AnhbgMASYVmSvP2lwU3545NyiR63g8J9PW3w=="], "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], - "pretty-format/@jest/types/@types/yargs": ["@types/yargs@15.0.20", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-KIkX+/GgfFitlASYCGoSF+T4XRXhOubJLhkLVtSfsRTe9jWMmuM2g28zQ41BtPTG7TRBb2xHW+LCNVE9QR/vsg=="], - - "react-native/@react-native/codegen/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="], - "react-native/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "react-native/metro-source-map/metro-symbolicate": ["metro-symbolicate@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.1", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-wPxYkONlq/Sv8Ji7vHEx5OzFouXAMQJjpcPW41ySKMLP/Ir18SsiJK2h4YkdKpYrTS1+0xf8oqF6nxCsT3uWtg=="], - - "react-native/metro-source-map/ob1": ["ob1@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-ngwqewtdUzFyycomdbdIhFLjePPSOt1awKMUXQ0L7iLHgWEPF3DsCerblzjzfAUHaXuvE9ccJymWQ/4PNNqvnQ=="], - - "react-native/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "react-native/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "react-native/react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], - - "restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], - "rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], @@ -3390,8 +2507,6 @@ "serve-static/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "shelljs/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "slice-ansi/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "string-length/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], @@ -3400,627 +2515,141 @@ "sucrase/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "temp/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "through2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-android/@react-native-community/cli-tools": ["@react-native-community/cli-tools@11.3.2", "", { "dependencies": { "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "find-up": "^5.0.0", "mime": "^2.4.1", "node-fetch": "^2.6.0", "open": "^6.2.0", "ora": "^5.4.1", "semver": "^6.3.0", "shell-quote": "^1.7.3" } }, "sha512-rAnFPzRITeEhBLwC73ucvWsYjsGyotSOI4c+k8t9wUqcIk1Q+RFnuWozGc13msOPdESvBHt2MPJBwXrtKgKn1g=="], + "@babel/highlight/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-android/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-ios/@react-native-community/cli-tools": ["@react-native-community/cli-tools@11.3.2", "", { "dependencies": { "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "find-up": "^5.0.0", "mime": "^2.4.1", "node-fetch": "^2.6.0", "open": "^6.2.0", "ora": "^5.4.1", "semver": "^6.3.0", "shell-quote": "^1.7.3" } }, "sha512-rAnFPzRITeEhBLwC73ucvWsYjsGyotSOI4c+k8t9wUqcIk1Q+RFnuWozGc13msOPdESvBHt2MPJBwXrtKgKn1g=="], + "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-ios/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-optimise-call-expression/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-ios/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], + "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-config/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-doctor/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], + "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro": ["metro@0.76.9", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "@babel/core": "^7.20.0", "@babel/generator": "^7.20.0", "@babel/parser": "^7.20.0", "@babel/template": "^7.0.0", "@babel/traverse": "^7.20.0", "@babel/types": "^7.20.0", "accepts": "^1.3.7", "async": "^3.2.2", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^2.2.0", "denodeify": "^1.2.1", "error-stack-parser": "^2.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.12.0", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^27.2.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.76.9", "metro-cache": "0.76.9", "metro-cache-key": "0.76.9", "metro-config": "0.76.9", "metro-core": "0.76.9", "metro-file-map": "0.76.9", "metro-inspector-proxy": "0.76.9", "metro-minify-uglify": "0.76.9", "metro-react-native-babel-preset": "0.76.9", "metro-resolver": "0.76.9", "metro-runtime": "0.76.9", "metro-source-map": "0.76.9", "metro-symbolicate": "0.76.9", "metro-transform-plugins": "0.76.9", "metro-transform-worker": "0.76.9", "mime-types": "^2.1.27", "node-fetch": "^2.2.0", "nullthrows": "^1.1.1", "rimraf": "^3.0.2", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "strip-ansi": "^6.0.0", "throat": "^5.0.0", "ws": "^7.5.1", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-gcjcfs0l5qIPg0lc5P7pj0x7vPJ97tan+OnEjiYLbKjR1D7Oa78CE93YUPyymUPH6q7VzlzMm1UjT35waEkZUw=="], + "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro-core": ["metro-core@0.76.9", "", { "dependencies": { "lodash.throttle": "^4.1.1", "metro-resolver": "0.76.9" } }, "sha512-DSeEr43Wrd5Q7ySfRzYzDwfV89g2OZTQDf1s3exOcLjE5fb7awoLOkA2h46ZzN8NcmbbM0cuJy6hOwF073/yRQ=="], + "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro-resolver": ["metro-resolver@0.76.9", "", {}, "sha512-s86ipNRas9vNR5lChzzSheF7HoaQEmzxBLzwFA6/2YcGmUCowcoyPAfs1yPh4cjMw9F1T4KlMLaiwniGE7HCyw=="], + "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro-runtime": ["metro-runtime@0.76.9", "", { "dependencies": { "@babel/runtime": "^7.0.0", "react-refresh": "^0.4.0" } }, "sha512-/5vezDpGUtA0Fv6cJg0+i6wB+QeBbvLeaw9cTSG7L76liP0b91f8vOcYzGaUbHI8pznJCCTerxRzpQ8e3/NcDw=="], + "@expo/cli/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-server-api/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + "@expo/cli/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - "@babel/highlight/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + "@expo/cli/ora/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], - "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + "@expo/cli/ora/cli-cursor/restore-cursor": ["restore-cursor@2.0.0", "", { "dependencies": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" } }, "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q=="], - "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "@expo/metro/metro-cache/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + "@expo/metro/metro/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="], - "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "@expo/metro/metro/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + "@expo/package-manager/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], - "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + "@expo/package-manager/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + "@expo/package-manager/ora/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], - "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + "@expo/package-manager/ora/cli-cursor/restore-cursor": ["restore-cursor@2.0.0", "", { "dependencies": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" } }, "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q=="], - "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + "@expo/xcpretty/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + "@jest/reporters/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "@react-native-community/cli-config/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + "@react-native-community/cli-platform-android/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + "@react-native-community/cli-platform-ios/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + "@react-native-community/cli-plugin-metro/metro-react-native-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.8.0", "", {}, "sha512-W6JDAOLZ5pMPMjEiQGLCXSSV7pIBEgRR5zGkxgmzGSXHOxqV5dC/M1Zevqpbm9TZDE5tu358qZf8Vkzmsc+u7Q=="], - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + "@react-native-community/cli-plugin-metro/metro-react-native-babel-transformer/metro-react-native-babel-preset/@babel/plugin-transform-flow-strip-types": ["@babel/plugin-transform-flow-strip-types@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-flow": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ=="], - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + "@react-native-community/cli-plugin-metro/metro-react-native-babel-transformer/metro-source-map/metro-symbolicate": ["metro-symbolicate@0.76.5", "", { "dependencies": { "invariant": "^2.2.4", "metro-source-map": "0.76.5", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "through2": "^2.0.1", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-7iftzh6G6HO4UDBmjsi2Yu4d6IkApv6Kg+jmBvkTjCXr8HwnKKum89gMg/FRMix+Rhhut0dnMpz6mAbtKTU9JQ=="], - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + "@react-native-community/cli-plugin-metro/metro-react-native-babel-transformer/metro-source-map/ob1": ["ob1@0.76.5", "", {}, "sha512-HoxZXMXNuY/eIXGoX7gx1C4O3eB4kJJMola6KoFaMm7PGGg39+AnhbgMASYVmSvP2lwU3545NyiR63g8J9PW3w=="], - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "@react-native-community/cli-server-api/pretty-format/@jest/types/@types/yargs": ["@types/yargs@15.0.20", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-KIkX+/GgfFitlASYCGoSF+T4XRXhOubJLhkLVtSfsRTe9jWMmuM2g28zQ41BtPTG7TRBb2xHW+LCNVE9QR/vsg=="], - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "@react-native-community/cli-tools/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + "@react-native/babel-plugin-codegen/@react-native/codegen/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "@react-native/babel-plugin-codegen/@react-native/codegen/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="], - "@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "@react-native/codegen/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + "@react-native/community-cli-plugin/@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - "@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "@react-native/community-cli-plugin/metro-config/metro-cache/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "@react-native/community-cli-plugin/metro/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="], - "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + "@react-native/community-cli-plugin/metro/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "@react-native/community-cli-plugin/metro/metro-cache/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "jest-config/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], + "jest-runtime/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.1", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw=="], + "logkitty/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + "logkitty/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "logkitty/yargs/yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], + "metro-config/metro/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.1", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw=="], + "metro-config/metro/metro-file-map/jest-regex-util": ["jest-regex-util@27.5.1", "", {}, "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg=="], - "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + "metro-config/metro/metro-file-map/jest-util": ["jest-util@27.5.1", "", { "dependencies": { "@jest/types": "^27.5.1", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw=="], - "@babel/plugin-transform-explicit-resource-management/@babel/plugin-transform-destructuring/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + "metro-config/metro/metro-source-map/ob1": ["ob1@0.76.9", "", {}, "sha512-g0I/OLnSxf6OrN3QjSew3bTDJCdbZoWxnh8adh1z36alwCuGF1dgDeRA25bTYSakrG5WULSaWJPOdgnf1O/oQw=="], - "@babel/plugin-transform-explicit-resource-management/@babel/plugin-transform-destructuring/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + "metro-config/metro/metro-transform-worker/metro-minify-terser": ["metro-minify-terser@0.76.9", "", { "dependencies": { "terser": "^5.15.0" } }, "sha512-ju2nUXTKvh96vHPoGZH/INhSvRRKM14CbGAJXQ98+g8K5z1v3luYJ/7+dFQB202eVzJdTB2QMtBjI1jUUpooCg=="], - "@babel/plugin-transform-explicit-resource-management/@babel/plugin-transform-destructuring/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + "metro-transform-worker/metro-cache/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "@babel/plugin-transform-explicit-resource-management/@babel/plugin-transform-destructuring/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + "metro-transform-worker/metro-cache/metro-core/metro-resolver": ["metro-resolver@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-t8j46kiILAqqFS5RNa+xpQyVjULxRxlvMidqUswPEk5nQVNdlJslqizDm/Et3v/JKwOtQGkYAQCHxP1zGStR/g=="], - "@babel/plugin-transform-explicit-resource-management/@babel/plugin-transform-destructuring/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + "metro-transform-worker/metro/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="], - "@babel/plugin-transform-explicit-resource-management/@babel/plugin-transform-destructuring/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "metro-transform-worker/metro/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + "metro/metro-file-map/jest-util/@jest/types": ["@jest/types@27.5.1", "", { "dependencies": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^16.0.0", "chalk": "^4.0.0" } }, "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw=="], - "@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + "metro/metro-file-map/jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - "@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + "metro/metro-file-map/jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + "metro/metro-react-native-babel-preset/@babel/plugin-transform-flow-strip-types/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - "@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + "metro/metro-react-native-babel-preset/@babel/plugin-transform-flow-strip-types/@babel/plugin-syntax-flow": ["@babel/plugin-syntax-flow@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ=="], - "@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + "pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], - "@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], - "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "react-native/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + "serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + "slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/plugin-transform-modules-systemjs/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-optimise-call-expression/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.1", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw=="], - - "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.1", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw=="], - - "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.1", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw=="], - - "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-computed-properties/@babel/template/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/preset-env/@babel/plugin-transform-computed-properties/@babel/template/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/preset-env/@babel/plugin-transform-computed-properties/@babel/template/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-destructuring/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/preset-env/@babel/plugin-transform-destructuring/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/preset-env/@babel/plugin-transform-destructuring/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/preset-env/@babel/plugin-transform-destructuring/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/preset-env/@babel/plugin-transform-destructuring/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/preset-env/@babel/plugin-transform-destructuring/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-function-name/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/preset-env/@babel/plugin-transform-function-name/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/preset-env/@babel/plugin-transform-function-name/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/preset-env/@babel/plugin-transform-function-name/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/preset-env/@babel/plugin-transform-function-name/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/preset-env/@babel/plugin-transform-function-name/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], - - "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], - - "@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@babel/preset-env/@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/preset-env/@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/preset-env/@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/preset-env/@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/preset-env/@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/preset-env/@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], - - "@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], - - "@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], - - "@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], - - "@expo/metro/metro-cache/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - - "@expo/metro/metro/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="], - - "@expo/metro/metro/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - - "@expo/xcpretty/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "@jest/reporters/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "@react-native-community/cli-clean/@react-native-community/cli-tools/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "@react-native-community/cli-clean/@react-native-community/cli-tools/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - - "@react-native-community/cli-clean/@react-native-community/cli-tools/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - - "@react-native-community/cli-clean/@react-native-community/cli-tools/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@react-native-community/cli-config/@react-native-community/cli-tools/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "@react-native-community/cli-config/@react-native-community/cli-tools/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - - "@react-native-community/cli-config/@react-native-community/cli-tools/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - - "@react-native-community/cli-config/@react-native-community/cli-tools/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@react-native-community/cli-config/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "@react-native-community/cli-doctor/@react-native-community/cli-platform-android/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "@react-native-community/cli-doctor/@react-native-community/cli-platform-ios/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "@react-native-community/cli-doctor/@react-native-community/cli-tools/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "@react-native-community/cli-doctor/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - - "@react-native-community/cli-hermes/@react-native-community/cli-platform-android/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "@react-native-community/cli-hermes/@react-native-community/cli-tools/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "@react-native-community/cli-hermes/@react-native-community/cli-tools/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - - "@react-native-community/cli-hermes/@react-native-community/cli-tools/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - - "@react-native-community/cli-hermes/@react-native-community/cli-tools/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@react-native-community/cli-platform-android/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "@react-native-community/cli-platform-ios/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "@react-native-community/cli-platform-ios/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - - "@react-native-community/cli-plugin-metro/@react-native-community/cli-tools/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "@react-native-community/cli-plugin-metro/@react-native-community/cli-tools/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - - "@react-native-community/cli-plugin-metro/@react-native-community/cli-tools/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - - "@react-native-community/cli-plugin-metro/@react-native-community/cli-tools/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@react-native-community/cli-plugin-metro/metro-react-native-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.8.0", "", {}, "sha512-W6JDAOLZ5pMPMjEiQGLCXSSV7pIBEgRR5zGkxgmzGSXHOxqV5dC/M1Zevqpbm9TZDE5tu358qZf8Vkzmsc+u7Q=="], - - "@react-native-community/cli-plugin-metro/metro-react-native-babel-transformer/metro-react-native-babel-preset/@babel/plugin-transform-flow-strip-types": ["@babel/plugin-transform-flow-strip-types@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-flow": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ=="], - - "@react-native-community/cli-server-api/@react-native-community/cli-tools/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "@react-native-community/cli-server-api/@react-native-community/cli-tools/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - - "@react-native-community/cli-server-api/@react-native-community/cli-tools/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - - "@react-native-community/cli-server-api/@react-native-community/cli-tools/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@react-native-community/cli-tools/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "@react-native-community/cli-tools/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - - "@react-native-community/cli/@react-native-community/cli-tools/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "@react-native-community/cli/@react-native-community/cli-tools/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - - "@react-native-community/cli/@react-native-community/cli-tools/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - - "@react-native-community/cli/@react-native-community/cli-tools/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@react-native/babel-plugin-codegen/@react-native/codegen/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "@react-native/babel-plugin-codegen/@react-native/codegen/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="], - - "@react-native/codegen/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "@react-native/community-cli-plugin/@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - - "@react-native/community-cli-plugin/metro-config/metro-cache/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - - "@react-native/community-cli-plugin/metro/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="], - - "@react-native/community-cli-plugin/metro/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - - "@react-native/community-cli-plugin/metro/metro-cache/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - - "@react-native/community-cli-plugin/metro/metro-source-map/ob1": ["ob1@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-ngwqewtdUzFyycomdbdIhFLjePPSOt1awKMUXQ0L7iLHgWEPF3DsCerblzjzfAUHaXuvE9ccJymWQ/4PNNqvnQ=="], - - "expo-router/@expo/metro-runtime/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "expo-router/@expo/metro-runtime/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "find-cache-dir/pkg-dir/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], - - "jest-config/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "jest-runtime/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "log-symbols/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "log-symbols/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - - "logkitty/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "logkitty/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - - "logkitty/yargs/yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - - "metro-config/metro/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "metro-config/metro/metro-file-map/jest-regex-util": ["jest-regex-util@27.5.1", "", {}, "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg=="], - - "metro-config/metro/metro-file-map/jest-util": ["jest-util@27.5.1", "", { "dependencies": { "@jest/types": "^27.5.1", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw=="], - - "metro-config/metro/metro-source-map/ob1": ["ob1@0.76.9", "", {}, "sha512-g0I/OLnSxf6OrN3QjSew3bTDJCdbZoWxnh8adh1z36alwCuGF1dgDeRA25bTYSakrG5WULSaWJPOdgnf1O/oQw=="], - - "metro-config/metro/metro-transform-worker/metro-minify-terser": ["metro-minify-terser@0.76.9", "", { "dependencies": { "terser": "^5.15.0" } }, "sha512-ju2nUXTKvh96vHPoGZH/INhSvRRKM14CbGAJXQ98+g8K5z1v3luYJ/7+dFQB202eVzJdTB2QMtBjI1jUUpooCg=="], - - "metro-transform-worker/metro-cache/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - - "metro-transform-worker/metro-cache/metro-core/metro-resolver": ["metro-resolver@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-t8j46kiILAqqFS5RNa+xpQyVjULxRxlvMidqUswPEk5nQVNdlJslqizDm/Et3v/JKwOtQGkYAQCHxP1zGStR/g=="], - - "metro-transform-worker/metro/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="], - - "metro-transform-worker/metro/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - - "metro/metro-file-map/jest-util/@jest/types": ["@jest/types@27.5.1", "", { "dependencies": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^16.0.0", "chalk": "^4.0.0" } }, "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw=="], - - "metro/metro-file-map/jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - - "metro/metro-file-map/jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "metro/metro-react-native-babel-preset/@babel/plugin-transform-flow-strip-types/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "metro/metro-react-native-babel-preset/@babel/plugin-transform-flow-strip-types/@babel/plugin-syntax-flow": ["@babel/plugin-syntax-flow@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ=="], - - "ora/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "ora/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - - "pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], - - "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], - - "react-native/@react-native/codegen/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="], - - "react-native/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "shelljs/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - - "temp/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-android/@react-native-community/cli-tools/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-android/@react-native-community/cli-tools/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-android/@react-native-community/cli-tools/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-android/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-ios/@react-native-community/cli-tools/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-ios/@react-native-community/cli-tools/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-ios/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-ios/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-ios/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-ios/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-config/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-doctor/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-doctor/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-doctor/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/metro-babel-transformer": ["metro-babel-transformer@0.76.9", "", { "dependencies": { "@babel/core": "^7.20.0", "hermes-parser": "0.12.0", "nullthrows": "^1.1.1" } }, "sha512-dAnAmBqRdTwTPVn4W4JrowPolxD1MDbuU97u3MqtWZgVRvDpmr+Cqnn5oSxLQk3Uc+Zy3wkqVrB/zXNRlLDSAQ=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/metro-cache-key": ["metro-cache-key@0.76.9", "", {}, "sha512-ugJuYBLngHVh1t2Jj+uP9pSCQl7enzVXkuh6+N3l0FETfqjgOaSHlcnIhMPn6yueGsjmkiIfxQU4fyFVXRtSTw=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/metro-file-map": ["metro-file-map@0.76.9", "", { "dependencies": { "anymatch": "^3.0.3", "debug": "^2.2.0", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-regex-util": "^27.0.6", "jest-util": "^27.2.0", "jest-worker": "^27.2.0", "micromatch": "^4.0.4", "node-abort-controller": "^3.1.1", "nullthrows": "^1.1.1", "walker": "^1.0.7" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, "sha512-7vJd8kksMDTO/0fbf3081bTrlw8SLiploeDf+vkkf0OwlrtDUWPOikfebp+MpZB2S61kamKjCNRfRkgrbPfSwg=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/metro-inspector-proxy": ["metro-inspector-proxy@0.76.9", "", { "dependencies": { "connect": "^3.6.5", "debug": "^2.2.0", "node-fetch": "^2.2.0", "ws": "^7.5.1", "yargs": "^17.6.2" }, "bin": { "metro-inspector-proxy": "src/cli.js" } }, "sha512-idIiPkb8CYshc0WZmbzwmr4B1QwsQUbpDwBzHwxE1ni27FWKWhV9CD5p+qlXZHgfwJuMRfPN+tIaLSR8+vttYg=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/metro-minify-uglify": ["metro-minify-uglify@0.76.9", "", { "dependencies": { "uglify-es": "^3.1.9" } }, "sha512-MXRrM3lFo62FPISlPfTqC6n9HTEI3RJjDU5SvpE7sJFfJKLx02xXQEltsL/wzvEqK+DhRQ5DEYACTwf5W4Z3yA=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/metro-source-map": ["metro-source-map@0.76.9", "", { "dependencies": { "@babel/traverse": "^7.20.0", "@babel/types": "^7.20.0", "invariant": "^2.2.4", "metro-symbolicate": "0.76.9", "nullthrows": "^1.1.1", "ob1": "0.76.9", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-q5qsMlu8EFvsT46wUUh+ao+efDsicT30zmaPATNhq+PcTawDbDgnMuUD+FT0bvxxnisU2PWl91RdzKfNc2qPQA=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/metro-symbolicate": ["metro-symbolicate@0.76.9", "", { "dependencies": { "invariant": "^2.2.4", "metro-source-map": "0.76.9", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "through2": "^2.0.1", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-Yyq6Ukj/IeWnGST09kRt0sBK8TwzGZWoU7YAcQlh14+AREH454Olx4wbFTpkkhUkV05CzNCvUuXQ0efFxhA1bw=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/metro-transform-plugins": ["metro-transform-plugins@0.76.9", "", { "dependencies": { "@babel/core": "^7.20.0", "@babel/generator": "^7.20.0", "@babel/template": "^7.0.0", "@babel/traverse": "^7.20.0", "nullthrows": "^1.1.1" } }, "sha512-YEQeNlOCt92I7S9A3xbrfaDfwfgcxz9PpD/1eeop3c4cO3z3Q3otYuxw0WJ/rUIW8pZfOm5XCehd+1NRbWlAaw=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/metro-transform-worker": ["metro-transform-worker@0.76.9", "", { "dependencies": { "@babel/core": "^7.20.0", "@babel/generator": "^7.20.0", "@babel/parser": "^7.20.0", "@babel/types": "^7.20.0", "babel-preset-fbjs": "^3.4.0", "metro": "0.76.9", "metro-babel-transformer": "0.76.9", "metro-cache": "0.76.9", "metro-cache-key": "0.76.9", "metro-minify-terser": "0.76.9", "metro-source-map": "0.76.9", "metro-transform-plugins": "0.76.9", "nullthrows": "^1.1.1" } }, "sha512-F69A0q0qFdJmP2Clqr6TpTSn4WTV9p5A28h5t9o+mB22ryXBZfUQ6BFBBW/6Wp2k/UtPH+oOsBfV9guiqm3d2Q=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], - - "@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - - "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function/@babel/template/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function/@babel/template/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/plugin-transform-explicit-resource-management/@babel/plugin-transform-destructuring/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/plugin-transform-explicit-resource-management/@babel/plugin-transform-destructuring/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/plugin-transform-explicit-resource-management/@babel/plugin-transform-destructuring/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/plugin-transform-modules-systemjs/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + "@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], @@ -4036,276 +2665,28 @@ "@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-optimise-call-expression/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-computed-properties/@babel/template/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-computed-properties/@babel/template/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-computed-properties/@babel/template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-destructuring/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-destructuring/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-destructuring/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-function-name/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-function-name/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-function-name/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.1", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw=="], - - "@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@babel/preset-env/@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + "@expo/cli/ora/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + "@expo/cli/ora/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "@expo/cli/ora/cli-cursor/restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="], - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + "@expo/package-manager/ora/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "@expo/package-manager/ora/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.1", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw=="], - - "@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@react-native-community/cli-clean/@react-native-community/cli-tools/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "@react-native-community/cli-clean/@react-native-community/cli-tools/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - - "@react-native-community/cli-config/@react-native-community/cli-tools/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "@react-native-community/cli-config/@react-native-community/cli-tools/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - - "@react-native-community/cli-doctor/@react-native-community/cli-platform-android/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "@react-native-community/cli-doctor/@react-native-community/cli-platform-ios/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "@react-native-community/cli-doctor/@react-native-community/cli-tools/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "@react-native-community/cli-hermes/@react-native-community/cli-platform-android/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "@react-native-community/cli-hermes/@react-native-community/cli-tools/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "@react-native-community/cli-hermes/@react-native-community/cli-tools/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - - "@react-native-community/cli-plugin-metro/@react-native-community/cli-tools/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "@react-native-community/cli-plugin-metro/@react-native-community/cli-tools/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], + "@expo/package-manager/ora/cli-cursor/restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="], "@react-native-community/cli-plugin-metro/metro-react-native-babel-transformer/metro-react-native-babel-preset/@babel/plugin-transform-flow-strip-types/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], "@react-native-community/cli-plugin-metro/metro-react-native-babel-transformer/metro-react-native-babel-preset/@babel/plugin-transform-flow-strip-types/@babel/plugin-syntax-flow": ["@babel/plugin-syntax-flow@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ=="], - "@react-native-community/cli-server-api/@react-native-community/cli-tools/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "@react-native-community/cli-server-api/@react-native-community/cli-tools/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - - "@react-native-community/cli/@react-native-community/cli-tools/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "@react-native-community/cli/@react-native-community/cli-tools/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - "@react-native/babel-plugin-codegen/@react-native/codegen/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "@react-native/community-cli-plugin/metro-config/metro-cache/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "@react-native/community-cli-plugin/metro/metro-cache/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "find-cache-dir/pkg-dir/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], - - "find-cache-dir/pkg-dir/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], - - "log-symbols/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - "metro-config/metro/metro-file-map/jest-util/@jest/types": ["@jest/types@27.5.1", "", { "dependencies": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^16.0.0", "chalk": "^4.0.0" } }, "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw=="], "metro-config/metro/metro-file-map/jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], @@ -4314,192 +2695,16 @@ "metro/metro-file-map/jest-util/@jest/types/@types/yargs": ["@types/yargs@16.0.11", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-sbtvk8wDN+JvEdabmZExoW/HNr1cB7D/j4LT08rMiuikfA7m/JNJg7ATQcgzs34zHnoScDkY0ZRSl29Fkmk36g=="], - "ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - "pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - "temp/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-android/@react-native-community/cli-tools/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-android/@react-native-community/cli-tools/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-android/@react-native-community/cli-tools/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-android/@react-native-community/cli-tools/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-android/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-ios/@react-native-community/cli-tools/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-ios/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-ios/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-config/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-doctor/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/@babel/traverse/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/metro-file-map/jest-regex-util": ["jest-regex-util@27.5.1", "", {}, "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/metro-file-map/jest-util": ["jest-util@27.5.1", "", { "dependencies": { "@jest/types": "^27.5.1", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/metro-source-map/ob1": ["ob1@0.76.9", "", {}, "sha512-g0I/OLnSxf6OrN3QjSew3bTDJCdbZoWxnh8adh1z36alwCuGF1dgDeRA25bTYSakrG5WULSaWJPOdgnf1O/oQw=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/metro-transform-worker/metro-minify-terser": ["metro-minify-terser@0.76.9", "", { "dependencies": { "terser": "^5.15.0" } }, "sha512-ju2nUXTKvh96vHPoGZH/INhSvRRKM14CbGAJXQ98+g8K5z1v3luYJ/7+dFQB202eVzJdTB2QMtBjI1jUUpooCg=="], - - "@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function/@babel/template/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function/@babel/template/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function/@babel/template/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "@expo/cli/ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "@expo/cli/ora/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + "@expo/package-manager/ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-optimise-call-expression/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-optimise-call-expression/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "find-cache-dir/pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + "@expo/package-manager/ora/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], "metro-config/metro/metro-file-map/jest-util/@jest/types/@types/yargs": ["@types/yargs@16.0.11", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-sbtvk8wDN+JvEdabmZExoW/HNr1cB7D/j4LT08rMiuikfA7m/JNJg7ATQcgzs34zHnoScDkY0ZRSl29Fkmk36g=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-android/@react-native-community/cli-tools/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-android/@react-native-community/cli-tools/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - - "@amazon-devices/keplerscript-appstore-iap-lib/react-native/@react-native-community/cli-platform-ios/@react-native-community/cli-tools/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/metro-file-map/jest-util/@jest/types": ["@jest/types@27.5.1", "", { "dependencies": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^16.0.0", "chalk": "^4.0.0" } }, "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/metro-file-map/jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/metro-file-map/jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function/@babel/template/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@amazon-devices/react-native-kepler/@react-native-community/cli/@react-native-community/cli-plugin-metro/metro/metro-file-map/jest-util/@jest/types/@types/yargs": ["@types/yargs@16.0.11", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-sbtvk8wDN+JvEdabmZExoW/HNr1cB7D/j4LT08rMiuikfA7m/JNJg7ATQcgzs34zHnoScDkY0ZRSl29Fkmk36g=="], } } diff --git a/libraries/expo-iap/example/package.json b/libraries/expo-iap/example/package.json index 6ae43289..d083d991 100644 --- a/libraries/expo-iap/example/package.json +++ b/libraries/expo-iap/example/package.json @@ -21,7 +21,6 @@ }, "dependencies": { "@expo/react-native-action-sheet": "^4.1.1", - "@amazon-devices/keplerscript-appstore-iap-lib": "~2.12.13", "@expo/vector-icons": "^15.0.2", "@preact/signals-react": "^3.2.1", "@react-navigation/bottom-tabs": "^7.2.0", @@ -50,10 +49,6 @@ }, "devDependencies": { "@babel/core": "^7.25.2", - "@amazon-devices/kepler-cli-platform": "~0.22.0", - "@amazon-devices/kepler-compatibility-metro-config": "^0.0.6", - "@amazon-devices/kepler-module-resolver-preset": "^0.1.15", - "@amazon-devices/react-native-kepler": "^2.0.0", "@react-native-community/cli": "11.3.2", "@react-native/metro-config": "^0.72.6", "@testing-library/jest-native": "^5.4.3", @@ -71,17 +66,6 @@ "typescript": "~5.9.2" }, "private": true, - "kepler": { - "projectType": "application", - "appName": "ExpoIAPExample", - "targets": [ - "tv" - ], - "os": [ - "vega" - ], - "api": 0.1 - }, "expo": { "autolinking": { "nativeModulesDir": ".." diff --git a/libraries/expo-iap/example/scripts/build-vega-example.mjs b/libraries/expo-iap/example/scripts/build-vega-example.mjs index baca3616..27a09e19 100644 --- a/libraries/expo-iap/example/scripts/build-vega-example.mjs +++ b/libraries/expo-iap/example/scripts/build-vega-example.mjs @@ -12,6 +12,10 @@ const tempPackageSourceRoot = path.join(tempRoot, 'openiap-expo-iap-src'); const buildType = process.argv[2] === 'Release' ? 'Release' : 'Debug'; const iapkitApiKey = process.env.EXPO_PUBLIC_IAPKIT_API_KEY ?? ''; const iapkitBaseUrl = process.env.EXPO_PUBLIC_IAPKIT_BASE_URL ?? ''; +const vegaPackageId = 'dev.hyo.openiap.expo.example'; +const vegaComponentId = `${vegaPackageId}.main`; +const vegaAppName = 'ExpoIapVegaExample'; +const vegaDisplayName = 'Expo IAP Vega Example'; const writeFile = (relativePath, contents) => { const filePath = path.join(tempRoot, relativePath); @@ -93,6 +97,44 @@ const writeLocalJavaScriptModule = (packageName, source, main = 'index.js') => { fs.writeFileSync(path.join(moduleRoot, main), source, 'utf8'); }; +const createVegaManifest = () => `schema-version = 1 + +[package] +id = "${vegaPackageId}" +title = "${vegaDisplayName}" +version = "1.0.0" + +[components] +[[components.interactive]] +id = "${vegaComponentId}" +runtime-module = "/com.amazon.kepler.keplerscript.runtime.loader_2@IKeplerScript_2_0" +launch-type = "singleton" +categories = ["com.amazon.category.main"] + +[wants] +[[wants.service]] +id = "com.amazon.inputmethod.service" + +[[wants.service]] +id = "com.amazon.network.service" + +[[wants.service]] +id = "com.amazon.iap.core.service" + +[[wants.service]] +id = "com.amazon.iap.tester.service" + +[[wants.module]] +id = "/com.amazon.iap.core@IIAPCoreUI" + +[[wants.module]] +id = "/com.amazonappstore.iap.tester@IIAPTesterUI" + +[needs] +[[needs.module]] +id = "/com.amazon.kepler.appstore.iap.purchase.core@IAppstoreIAPPurchaseCoreService" +`; + const rewriteExpoSourceImports = (source) => source .replaceAll("from '../../src/utils/errorMapping'", "from 'expo-iap'") @@ -297,7 +339,7 @@ writeFile( }, kepler: { projectType: 'application', - appName: 'ExpoIapVegaExample', + appName: vegaAppName, targets: ['tv'], os: ['vega'], }, @@ -312,8 +354,8 @@ writeFile( `${JSON.stringify( { '//': 'The declared app name must match the Vega component id.', - name: 'dev.hyo.openiap.expo.example.main', - displayName: 'Expo IAP Vega Example', + name: vegaComponentId, + displayName: vegaDisplayName, }, null, 2, @@ -405,7 +447,7 @@ writeLocalEntryModule( 'openiap-expo-iap.ts', path.join(tempPackageSourceRoot, 'index.kepler.ts'), ); -copyFile(path.join(exampleRoot, 'manifest.toml'), 'manifest.toml'); +writeFile('manifest.toml', createVegaManifest()); run('bun', ['install', '--force']); writeLocalPackageAlias( diff --git a/libraries/expo-iap/plugin/__tests__/withIAP.test.ts b/libraries/expo-iap/plugin/__tests__/withIAP.test.ts index 28406a61..3136fb9e 100644 --- a/libraries/expo-iap/plugin/__tests__/withIAP.test.ts +++ b/libraries/expo-iap/plugin/__tests__/withIAP.test.ts @@ -495,7 +495,7 @@ describe('vega project generation', () => { }); }); - it('merges Vega scripts, dependencies, and kepler metadata', () => { + it('merges Vega scripts, dependency buckets, and kepler metadata', () => { const settings = resolveVegaProjectSettings({ name: 'Expo IAP Example', slug: 'expo-iap-example', @@ -528,6 +528,82 @@ describe('vega project generation', () => { expect( result.devDependencies?.['@amazon-devices/kepler-cli-platform'], ).toBe('~0.22.0'); + expect( + result.devDependencies?.['@amazon-devices/react-native-kepler'], + ).toBeUndefined(); + expect( + result.optionalDependencies?.['@amazon-devices/react-native-kepler'], + ).toBe('^2.0.0'); expect(result.kepler?.appName).toBe('ExpoIAPExample'); }); + + it('moves existing react-native-kepler direct dependency into optionalDependencies', () => { + const settings = resolveVegaProjectSettings({ + name: 'Expo IAP Example', + slug: 'expo-iap-example', + android: {package: 'dev.hyo.martie'}, + } as ExpoConfig); + const result = mergeVegaPackageJson( + { + dependencies: { + '@amazon-devices/keplerscript-appstore-iap-lib': '~2.12.13', + }, + devDependencies: { + '@amazon-devices/kepler-cli-platform': '~0.22.0', + '@amazon-devices/react-native-kepler': '^2.0.0', + }, + }, + settings, + ); + + expect( + result.dependencies?.['@amazon-devices/keplerscript-appstore-iap-lib'], + ).toBe('~2.12.13'); + expect( + result.devDependencies?.['@amazon-devices/kepler-cli-platform'], + ).toBe('~0.22.0'); + expect( + result.devDependencies?.['@amazon-devices/react-native-kepler'], + ).toBeUndefined(); + expect( + result.optionalDependencies?.['@amazon-devices/react-native-kepler'], + ).toBe('^2.0.0'); + }); + + it('moves Vega CLI tooling out of optionalDependencies for command discovery', () => { + const settings = resolveVegaProjectSettings({ + name: 'Expo IAP Example', + slug: 'expo-iap-example', + android: {package: 'dev.hyo.martie'}, + } as ExpoConfig); + const result = mergeVegaPackageJson( + { + optionalDependencies: { + '@amazon-devices/kepler-cli-platform': '~0.22.0', + '@amazon-devices/kepler-compatibility-metro-config': '^0.0.6', + '@amazon-devices/kepler-module-resolver-preset': '^0.1.15', + '@amazon-devices/react-native-kepler': '^2.0.0', + }, + }, + settings, + ); + + expect( + result.devDependencies?.['@amazon-devices/kepler-cli-platform'], + ).toBe('~0.22.0'); + expect( + result.devDependencies?.[ + '@amazon-devices/kepler-compatibility-metro-config' + ], + ).toBe('^0.0.6'); + expect( + result.devDependencies?.['@amazon-devices/kepler-module-resolver-preset'], + ).toBe('^0.1.15'); + expect( + result.optionalDependencies?.['@amazon-devices/kepler-cli-platform'], + ).toBeUndefined(); + expect( + result.optionalDependencies?.['@amazon-devices/react-native-kepler'], + ).toBe('^2.0.0'); + }); }); diff --git a/libraries/expo-iap/plugin/src/withVega.ts b/libraries/expo-iap/plugin/src/withVega.ts index ff2f2083..cc0c31b5 100644 --- a/libraries/expo-iap/plugin/src/withVega.ts +++ b/libraries/expo-iap/plugin/src/withVega.ts @@ -178,6 +178,7 @@ type MutablePackageJson = { scripts?: Record; dependencies?: Record; devDependencies?: Record; + optionalDependencies?: Record; kepler?: Record; }; @@ -191,6 +192,41 @@ const setIfMissing = ( } }; +const setDependency = (pkg: MutablePackageJson, key: string, value: string) => { + delete pkg.devDependencies?.[key]; + delete pkg.optionalDependencies?.[key]; + if (!pkg.dependencies) { + pkg.dependencies = {}; + } + setIfMissing(pkg.dependencies, key, value); +}; + +const setDevDependency = ( + pkg: MutablePackageJson, + key: string, + value: string, +) => { + delete pkg.dependencies?.[key]; + delete pkg.optionalDependencies?.[key]; + if (!pkg.devDependencies) { + pkg.devDependencies = {}; + } + setIfMissing(pkg.devDependencies, key, value); +}; + +const setOptionalDependency = ( + pkg: MutablePackageJson, + key: string, + value: string, +) => { + delete pkg.dependencies?.[key]; + delete pkg.devDependencies?.[key]; + if (!pkg.optionalDependencies) { + pkg.optionalDependencies = {}; + } + setIfMissing(pkg.optionalDependencies, key, value); +}; + const getVpkgBaseName = ( pkg: MutablePackageJson, settings: VegaProjectSettings, @@ -207,11 +243,12 @@ const getVpkgBaseName = ( export const mergeVegaPackageJson = ( pkg: T, settings: VegaProjectSettings, -): T => { - const next = {...pkg}; +): T & MutablePackageJson => { + const next: T & MutablePackageJson = {...pkg}; next.scripts = {...(pkg.scripts ?? {})}; next.dependencies = {...(pkg.dependencies ?? {})}; next.devDependencies = {...(pkg.devDependencies ?? {})}; + next.optionalDependencies = {...(pkg.optionalDependencies ?? {})}; const vpkgBaseName = getVpkgBaseName(pkg, settings); setIfMissing( @@ -235,32 +272,24 @@ export const mergeVegaPackageJson = ( `vega device install-app --packagePath build/armv7-debug/${vpkgBaseName}_armv7.vpkg && vega device launch-app --appName ${settings.componentId}`, ); - setIfMissing( - next.dependencies, + setDependency( + next, '@amazon-devices/keplerscript-appstore-iap-lib', '~2.12.13', ); - setIfMissing( - next.devDependencies, - '@amazon-devices/kepler-cli-platform', - '~0.22.0', - ); - setIfMissing( - next.devDependencies, + setDevDependency(next, '@amazon-devices/kepler-cli-platform', '~0.22.0'); + setDevDependency( + next, '@amazon-devices/kepler-compatibility-metro-config', '^0.0.6', ); - setIfMissing( - next.devDependencies, + setDevDependency( + next, '@amazon-devices/kepler-module-resolver-preset', '^0.1.15', ); - setIfMissing( - next.devDependencies, - '@amazon-devices/react-native-kepler', - '^2.0.0', - ); + setOptionalDependency(next, '@amazon-devices/react-native-kepler', '^2.0.0'); setIfMissing(next.devDependencies, '@react-native-community/cli', '11.3.2'); setIfMissing(next.devDependencies, '@react-native/metro-config', '^0.72.6'); setIfMissing(next.devDependencies, 'babel-plugin-module-resolver', '^5.0.2'); diff --git a/libraries/expo-iap/src/ExpoIapModule.ts b/libraries/expo-iap/src/ExpoIapModule.ts index a25c2c17..468e60bd 100644 --- a/libraries/expo-iap/src/ExpoIapModule.ts +++ b/libraries/expo-iap/src/ExpoIapModule.ts @@ -42,7 +42,7 @@ function getResolved(): {module: any; name: NativeIapModuleName} { if (!vegaModule) { throw new UnavailabilityError( 'expo-iap', - 'Amazon Vega IAP module is unavailable. Add @amazon-devices/keplerscript-appstore-iap-lib and build with the React Native Vega kepler platform.', + 'Amazon Vega IAP module is unavailable. Install @amazon-devices/keplerscript-appstore-iap-lib in the Vega app target and build with the React Native for Vega kepler platform.', ); } return {module: vegaModule, name: 'ExpoIapVega'}; diff --git a/libraries/expo-iap/src/index.kepler.ts b/libraries/expo-iap/src/index.kepler.ts index 10832da0..3b4834e4 100644 --- a/libraries/expo-iap/src/index.kepler.ts +++ b/libraries/expo-iap/src/index.kepler.ts @@ -37,7 +37,7 @@ const getModule = () => { const module = getVegaIapModule(); if (!module) { throw new Error( - 'Amazon Vega IAP module is unavailable. Add @amazon-devices/keplerscript-appstore-iap-lib and build with the React Native Vega kepler platform.', + 'Amazon Vega IAP module is unavailable. Install @amazon-devices/keplerscript-appstore-iap-lib in the Vega app target and build with the React Native for Vega kepler platform.', ); } return module; @@ -65,7 +65,10 @@ const normalizePurchaseArray = (purchases: Purchase[]): Purchase[] => }); const getAndroidRequest = ( - request?: RequestPurchasePropsByPlatforms | RequestSubscriptionPropsByPlatforms | null, + request?: + | RequestPurchasePropsByPlatforms + | RequestSubscriptionPropsByPlatforms + | null, ) => request?.google ?? request?.android; const createPurchaseTokenError = (purchase: Purchase): Error => { diff --git a/libraries/react-native-iap/src/index.kepler.ts b/libraries/react-native-iap/src/index.kepler.ts index 07e070f8..9e02e91e 100644 --- a/libraries/react-native-iap/src/index.kepler.ts +++ b/libraries/react-native-iap/src/index.kepler.ts @@ -53,7 +53,7 @@ const getModule = () => { const module = getVegaIapModule(); if (!module) { throw new Error( - 'Amazon Vega IAP module is unavailable. Add @amazon-devices/keplerscript-appstore-iap-lib and build with the React Native Vega kepler platform.', + 'Amazon Vega IAP module is unavailable. Install @amazon-devices/keplerscript-appstore-iap-lib in the Vega app target and build with the React Native for Vega kepler platform.', ); } return module; diff --git a/libraries/react-native-iap/src/index.ts b/libraries/react-native-iap/src/index.ts index a67ab132..ffd14746 100644 --- a/libraries/react-native-iap/src/index.ts +++ b/libraries/react-native-iap/src/index.ts @@ -217,7 +217,7 @@ const IAP = { const vegaModule = getVegaIapModule(); if (!vegaModule) { throw new Error( - 'Amazon Vega IAP module is unavailable. Add @amazon-devices/keplerscript-appstore-iap-lib and build with the React Native Vega kepler platform.', + 'Amazon Vega IAP module is unavailable. Install @amazon-devices/keplerscript-appstore-iap-lib in the Vega app target and build with the React Native for Vega kepler platform.', ); } iapRef = vegaModule; diff --git a/packages/docs/src/pages/docs/features/vega-os.tsx b/packages/docs/src/pages/docs/features/vega-os.tsx index 0a22bbf3..12b2e9cf 100644 --- a/packages/docs/src/pages/docs/features/vega-os.tsx +++ b/packages/docs/src/pages/docs/features/vega-os.tsx @@ -88,12 +88,11 @@ function VegaOSRuntime() { .
    • - Amazon Vega IAP package installed in the app: - {`{ - "dependencies": { - "@amazon-devices/keplerscript-appstore-iap-lib": "~2.12.13" - } -}`} + Amazon Vega IAP installed in the Vega app target.{' '} + react-native-iap and expo-iap declare the + Amazon IAP package as an optional peer dependency, so non-Vega iOS, + Android, Fire OS, and Horizon builds do not install it by default: + {`npm install @amazon-devices/keplerscript-appstore-iap-lib@~2.12.13`}
    • Vega IAP service declarations in manifest.toml: @@ -128,6 +127,21 @@ id = "/com.amazon.kepler.appstore.iap.purchase.core@IAppstoreIAPPurchaseCoreServ metadata. Plain React Native apps should provide the Vega project files directly, as shown in the OpenIAP repository example.

      +

      + Keep the React Native for Vega runtime in the Vega app target or a + Vega-only package manifest. In particular, do not keep{' '} + @amazon-devices/react-native-kepler as a direct + dependency of a package manifest that is also used for normal iOS or + Android builds, because React Native Codegen can scan it during those + builds. +

      +

      + If a Vega target keeps{' '} + @amazon-devices/react-native-kepler in{' '} + optionalDependencies, do not omit optional dependencies + during install. The package is still required for Vega builds; it is + optionalized only to keep non-Vega installs and Codegen clean. +

      react-native-iap @@ -141,26 +155,49 @@ id = "/com.amazon.kepler.appstore.iap.purchase.core@IAppstoreIAPPurchaseCoreServ kepler runtime. Non-Vega platforms continue creating the Nitro RnIap HybridObject.

      - {`[ - 'react-native-iap', - { - amazon: { - fireOS: true, - vegaOS: true, - }, - modules: { - horizon: false, - }, - }, -]`}

      - In react-native-iap, amazon.fireOS selects - the Android Amazon Appstore flavor during prebuild.{' '} - amazon.vegaOS keeps the Amazon target shape aligned with{' '} - expo-iap, but the React Native app still supplies its - Vega manifest.toml, entry point, and Kepler build target - directly. + Vega users install the Amazon IAP package in their Vega app target to + satisfy the optional peer dependency. Projects that also ship regular + iOS or Android apps should isolate the Amazon React Native for Vega + runtime packages in the Vega target, as the repository example build + script does with a temporary React Native 0.72 app. +

      +

      + In react-native-iap, the config plugin can select the + Fire OS Android flavor with amazon.fireOS, but it does + not generate Vega project files or automatically sync package.json + dependencies. A plain React Native Vega target should provide its own{' '} + manifest.toml, Kepler entry point, Metro/Babel resolver, + package dependencies, and kepler metadata directly.

      + {`# In the Vega-only React Native for Vega target +yarn add react-native-iap +yarn add @amazon-devices/keplerscript-appstore-iap-lib@~2.12.13 @amazon-devices/react-native-kepler@^2.0.0 +yarn add -D @amazon-devices/kepler-cli-platform@~0.22.0 @react-native-community/cli@11.3.2 @react-native/metro-config@^0.72.6`} +

      + A Vega-only package manifest can keep the React Native for Vega + runtime as a direct dependency because that manifest is not used by + normal iOS or Android builds: +

      + {`{ + "dependencies": { + "@amazon-devices/keplerscript-appstore-iap-lib": "~2.12.13", + "@amazon-devices/react-native-kepler": "^2.0.0", + "react": "18.2.0", + "react-native": "0.72.0" + }, + "devDependencies": { + "@amazon-devices/kepler-cli-platform": "~0.22.0", + "@react-native-community/cli": "11.3.2", + "@react-native/metro-config": "^0.72.6" + }, + "kepler": { + "projectType": "application", + "appName": "MyVegaApp", + "targets": ["tv"], + "os": ["vega"] + } +}`}

      The React Native example includes a Vega build script that creates an Amazon-supported React Native 0.72 Vega build target, copies the @@ -186,6 +223,14 @@ yarn run:vega:firetv`} against a React Native for Vega runtime version supported by the installed Amazon Vega CLI.

      +

      + Install the Amazon Vega peer dependency only for the Vega build + target. The regular Expo iOS and Android development build should not + require @amazon-devices/react-native-kepler unless that + same package manifest is intentionally building a Vega artifact. Vega + CI should also install optional dependencies so the React Native for + Vega runtime is present for build-vega. +

      {`[ 'expo-iap', { @@ -204,9 +249,13 @@ yarn run:vega:firetv`} When amazon.vegaOS is enabled, the Expo plugin prepares the Vega manifest, entry point, generated app metadata, app icon assets, Kepler package metadata, and Vega build scripts during - prebuild. amazon.fireOS can be enabled in the same - config, but Fire OS and Vega OS still produce separate build - artifacts. + prebuild. It keeps the Amazon IAP package as a runtime dependency and + the Kepler CLI/Metro/Babel packages as development dependencies, but + syncs @amazon-devices/react-native-kepler as an{' '} + optionalDependency so regular iOS and Android Codegen do + not scan it as a direct React Native dependency.{' '} + amazon.fireOS can be enabled in the same config, but Fire + OS and Vega OS still produce separate build artifacts.

      {`EXPO_IAP_VEGA=1 expo prebuild --platform android --no-install EXPO_IAP_VEGA=1 react-native build-vega --build-type Debug`} diff --git a/packages/docs/src/pages/docs/setup/expo.tsx b/packages/docs/src/pages/docs/setup/expo.tsx index 4f33d02f..a69bcfda 100644 --- a/packages/docs/src/pages/docs/setup/expo.tsx +++ b/packages/docs/src/pages/docs/setup/expo.tsx @@ -308,6 +308,17 @@ cd ios && pod install`} They can both be true in one config, but Fire OS and Vega OS are still built as separate artifacts.

      +

      + Vega OS support uses optional peer dependencies. Install Amazon's Vega + IAP package only in the Vega app target. When{' '} + amazon.vegaOS is enabled, the plugin keeps the Kepler + CLI, Metro, and Babel packages available for build-vega, + but syncs @amazon-devices/react-native-kepler as an{' '} + optionalDependency. Keep that package out of normal{' '} + dependencies and devDependencies used by + regular Expo iOS or Android builds, and make sure Vega CI installs + optional dependencies before running build-vega. +

      diff --git a/packages/docs/src/pages/docs/setup/react-native.tsx b/packages/docs/src/pages/docs/setup/react-native.tsx index c3840177..c9574fdf 100644 --- a/packages/docs/src/pages/docs/setup/react-native.tsx +++ b/packages/docs/src/pages/docs/setup/react-native.tsx @@ -200,12 +200,72 @@ end`} Fire OS Setup Guide.
    • - For Vega OS, do not use an Android flavor. Use{' '} - amazon.vegaOS=true as the config plugin target marker, - install Amazon's Vega IAP package, and follow the{' '} + For Vega OS, do not use an Android flavor. Create a React Native for + Vega target with its own package manifest, install Amazon's Vega + packages only in that target, and follow the{' '} Vega OS Runtime guide.
    • + +

      + Vega OS + + # + +

      +

      + react-native-iap declares Amazon Vega runtime packages as + optional peer dependencies, so normal iOS, Android, Fire OS, and + Horizon installs do not need to install them. Unlike{' '} + expo-iap, the react-native-iap config plugin + does not generate a Vega manifest.toml, entry file, build + scripts, or package dependency sync during prebuild. +

      +

      + Plain React Native apps should keep Vega dependencies in a Vega-only + package manifest. The Kepler CLI package must be available as a + development dependency in that Vega target so the React Native CLI can + discover build-vega. Keep{' '} + @amazon-devices/react-native-kepler out of normal + iOS/Android dependencies and devDependencies{' '} + to avoid regular React Native Codegen scanning it. +

      + {`# In the Vega-only React Native for Vega target +yarn add react-native-iap +yarn add @amazon-devices/keplerscript-appstore-iap-lib@~2.12.13 @amazon-devices/react-native-kepler@^2.0.0 +yarn add -D @amazon-devices/kepler-cli-platform@~0.22.0 @react-native-community/cli@11.3.2 @react-native/metro-config@^0.72.6`} +

      + A Vega-only package manifest can keep the React Native for Vega + runtime as a direct dependency because that manifest is not used by + normal iOS or Android builds: +

      + {`{ + "dependencies": { + "@amazon-devices/keplerscript-appstore-iap-lib": "~2.12.13", + "@amazon-devices/react-native-kepler": "^2.0.0", + "react": "18.2.0", + "react-native": "0.72.0" + }, + "devDependencies": { + "@amazon-devices/kepler-cli-platform": "~0.22.0", + "@react-native-community/cli": "11.3.2", + "@react-native/metro-config": "^0.72.6" + }, + "kepler": { + "projectType": "application", + "appName": "MyVegaApp", + "targets": ["tv"], + "os": ["vega"] + } +}`} +

      + The repository example follows this isolation model by generating a + temporary React Native 0.72 Vega project before running{' '} + react-native build-vega: +

      + {`cd libraries/react-native-iap/example +yarn build:vega:debug +yarn run:vega:firetv`}
      diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt index c904c5d6..06016419 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt @@ -23,7 +23,6 @@ import dev.hyo.martie.IapConstants import dev.hyo.martie.models.AppColors import dev.hyo.martie.screens.uis.* import dev.hyo.openiap.IapContext -import dev.hyo.openiap.OpenIapError import dev.hyo.openiap.store.OpenIapStore import dev.hyo.openiap.store.PurchaseResultStatus import kotlinx.coroutines.CoroutineScope @@ -105,6 +104,46 @@ fun PurchaseFlowScreen( runCatching { BuildConfig.IAPKIT_API_KEY.takeIf { it.isNotBlank() } }.getOrNull() } + fun purchasePropsFor(product: ProductAndroid): RequestPurchaseProps = + if (product.type == ProductType.Subs) { + RequestPurchaseProps( + request = RequestPurchaseProps.Request.Subscription( + RequestSubscriptionPropsByPlatforms( + android = RequestSubscriptionAndroidProps( + skus = listOf(product.id) + ) + ) + ), + type = ProductQueryType.Subs + ) + } else { + RequestPurchaseProps( + request = RequestPurchaseProps.Request.Purchase( + RequestPurchasePropsByPlatforms( + android = RequestPurchaseAndroidProps( + skus = listOf(product.id) + ) + ) + ), + type = ProductQueryType.InApp + ) + } + + fun launchPurchase(product: ProductAndroid) { + uiScope.launch { + iapStore.setActivity(activity) + try { + iapStore.requestPurchase(purchasePropsFor(product)) + } catch (e: Exception) { + iapStore.postStatusMessage( + message = e.message ?: "Purchase failed", + status = PurchaseResultStatus.Error, + productId = product.id + ) + } + } + } + // Use a dedicated scope for cleanup that won't be cancelled with composition val cleanupScope = remember { CoroutineScope(Dispatchers.Main + SupervisorJob()) } @@ -448,36 +487,7 @@ fun PurchaseFlowScreen( ProductCard( product = androidProduct, isPurchasing = status.isPurchasing(androidProduct.id), - onPurchase = { - scope.launch { - iapStore.setActivity(activity) - if (androidProduct.type == ProductType.Subs) { - val props = RequestPurchaseProps( - request = RequestPurchaseProps.Request.Subscription( - RequestSubscriptionPropsByPlatforms( - android = RequestSubscriptionAndroidProps( - skus = listOf(androidProduct.id) - ) - ) - ), - type = ProductQueryType.Subs - ) - iapStore.requestPurchase(props) - } else { - val props = RequestPurchaseProps( - request = RequestPurchaseProps.Request.Purchase( - RequestPurchasePropsByPlatforms( - android = RequestPurchaseAndroidProps( - skus = listOf(androidProduct.id) - ) - ) - ), - type = ProductQueryType.InApp - ) - iapStore.requestPurchase(props) - } - } - }, + onPurchase = { launchPurchase(androidProduct) }, onClick = { selectedProduct = androidProduct }, @@ -724,36 +734,7 @@ fun PurchaseFlowScreen( ProductDetailModal( product = product, onDismiss = { selectedProduct = null }, - onPurchase = { - uiScope.launch { - iapStore.setActivity(activity) - if (product.type == ProductType.Subs) { - val props = RequestPurchaseProps( - request = RequestPurchaseProps.Request.Subscription( - RequestSubscriptionPropsByPlatforms( - android = RequestSubscriptionAndroidProps( - skus = listOf(product.id) - ) - ) - ), - type = ProductQueryType.Subs - ) - iapStore.requestPurchase(props) - } else { - val props = RequestPurchaseProps( - request = RequestPurchaseProps.Request.Purchase( - RequestPurchasePropsByPlatforms( - android = RequestPurchaseAndroidProps( - skus = listOf(product.id) - ) - ) - ), - type = ProductQueryType.InApp - ) - iapStore.requestPurchase(props) - } - } - }, + onPurchase = { launchPurchase(product) }, isPurchasing = status.isPurchasing(product.id) ) } From c3fa895880e6befa967936cdd8f4529c8660d454 Mon Sep 17 00:00:00 2001 From: Hyo Date: Thu, 11 Jun 2026 22:25:44 +0900 Subject: [PATCH 48/51] chore(workflows): add device e2e regression runbook Add a Claude/Codex e2e-tests workflow for connected-device OpenIAP regression across Expo, React Native, FireOS, iOS, Android, and Vega paths. Make AGENTS.md the root instruction SSOT with CLAUDE.md and GEMINI.md symlinked to it, and route the workflow from the Codex skill. --- .claude/commands/commit.md | 2 +- .claude/commands/e2e-tests.md | 303 +++++++++++++++++++++++ .claude/commands/verify-all.md | 5 +- .codex/skills/openiap-workflows/SKILL.md | 18 +- AGENTS.md | 197 ++++++++++++++- CLAUDE.md | 195 +-------------- GEMINI.md | 2 +- 7 files changed, 515 insertions(+), 207 deletions(-) create mode 100644 .claude/commands/e2e-tests.md mode change 120000 => 100644 AGENTS.md mode change 100644 => 120000 CLAUDE.md diff --git a/.claude/commands/commit.md b/.claude/commands/commit.md index cb617d4f..b07c0ed1 100644 --- a/.claude/commands/commit.md +++ b/.claude/commands/commit.md @@ -34,7 +34,7 @@ Complete workflow: branch → commit → push → PR If the staged changes only touch internal agent/workflow files, do not push or create a PR unless the user explicitly asked to publish, PR, or merge them. Internal workflow files include `.claude/commands/`, `.codex/skills/`, -`AGENTS.md`, `CLAUDE.md`, and agent automation notes. +`AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, and agent automation notes. For those internal-only changes, prefer a local commit or local working-tree change and report the files changed. If the user explicitly asks to open or diff --git a/.claude/commands/e2e-tests.md b/.claude/commands/e2e-tests.md new file mode 100644 index 00000000..ba3e98d3 --- /dev/null +++ b/.claude/commands/e2e-tests.md @@ -0,0 +1,303 @@ +# E2E Tests — Device-Backed OpenIAP Regression + +Run this when a PR or release candidate needs real-device regression across +OpenIAP native packages and framework examples. + +## Scope + +Use the narrowest scope that satisfies the request, but broaden when native +build configuration, dependency placement, config plugins, package manifests, +or store-specific behavior changed. + +- **Full PR regression**: package checks, Expo checks, React Native checks, + Android/FireOS/iOS builds, Vega debug/release builds, and manual purchase + smoke flows on connected devices. +- **Build-only regression**: all relevant compile/build commands, no purchase + dialogs. Clearly report that purchase flows were not exercised. +- **Store-flow regression**: build/install each requested target, then verify + product fetch, purchase, finish/consume, restore/get available purchases, and + cancel/error handling where the example UI exposes them. +- **Vega/FireOS dependency regression**: run normal Expo/RN iOS and Android + builds first, then FireOS/Amazon and Vega builds. This catches accidental + Kepler dependency leakage. + +## Rules + +- Check `git status --short --branch` before changes. Do not revert user + changes. +- Do not report a platform as green without a passing command or a concrete + manual-device result. +- If a build fails, fix it and rerun the failing command plus the adjacent + platform command that could regress. +- Use connected physical devices when available and record serials/UDIDs. +- Approve sandbox/test purchase dialogs only when the current user request + explicitly authorizes purchase approval. +- Treat missing store account, catalog, entitlement, tester app, or device + connectivity as blocked, not passed. +- For VegaOS, source `~/vega/env` before invoking `vega` when present. +- For Expo Vega, verify normal Expo iOS/Android manifests do not require + Vega-only dependencies unless `amazon.vegaOS=true` or a generated Vega temp + target requires them. +- For React Native Vega, remember there is no Expo config plugin. RN users need + a Vega-only target/package manifest; normal iOS/Android manifests must not + require Kepler packages. + +## Preflight + +Run from the repo root: + +```bash +git status --short --branch +git branch --show-current +git log --oneline --decorate -5 +if command -v adb >/dev/null 2>&1; then + adb devices -l +else + echo "adb: not found" +fi +if command -v xcrun >/dev/null 2>&1; then + xcrun devicectl list devices +else + echo "xcrun: not found" +fi +if [ -f "$HOME/vega/env" ]; then source "$HOME/vega/env"; fi +if command -v vega >/dev/null 2>&1; then + vega -v --json + vega exec vda devices +else + echo "vega: not found" +fi +node -v +bun -v +PATH="$HOME/.bun/bin:$PATH" bun -v +(cd libraries/react-native-iap && yarn -v) +java -version +if command -v xcodebuild >/dev/null 2>&1; then + xcodebuild -version +else + echo "xcodebuild: not found" +fi +``` + +If the repo expects the bundled Bun version, prefer this from the repo root: + +```bash +PATH="$HOME/.bun/bin:$PATH" bun run audit:parity +``` + +## Package-Level Checks + +Google Android package: + +```bash +cd packages/google +./gradlew :openiap:compilePlayDebugKotlin \ + :openiap:compileHorizonDebugKotlin \ + :openiap:compileAmazonDebugKotlin \ + :Example:compileAmazonDebugKotlin \ + :Example:compilePlayDebugKotlin \ + :openiap:test +``` + +Apple package: + +```bash +cd packages/apple +swift test +``` + +Docs and parity when docs, API surface, generated types, examples, or package +parity changed: + +```bash +# Run from the repo root. +PATH="$HOME/.bun/bin:$PATH" bun audit:docs +PATH="$HOME/.bun/bin:$PATH" bun audit:parity +``` + +## Expo Checks + +Library and plugin: + +```bash +cd libraries/expo-iap +bun run lint:tsc +cd plugin +bunx jest --runInBand +``` + +Example tests, normal Android build, and launch smoke: + +```bash +cd libraries/expo-iap/example +bun run test --runInBand +# If Expo config, plugin, or native dependency wiring changed, run +# `bunx expo prebuild --platform android --clean` instead. +test -d android || bunx expo prebuild --platform android +cd android +./gradlew :app:assembleDebug +# Build-only regression can stop here. +: "${ANDROID_SERIAL:?Set ANDROID_SERIAL to the target Android device serial}" +adb -s "$ANDROID_SERIAL" install -r app/build/outputs/apk/debug/app-debug.apk +adb -s "$ANDROID_SERIAL" shell monkey -p dev.hyo.openiap.expo.example 1 +``` + +Normal iOS physical-device build and launch smoke: + +```bash +cd libraries/expo-iap/example +: "${IOS_UDID:?Set IOS_UDID to the target iOS device UDID}" +: "${TEAM_ID:?Set TEAM_ID to the Apple development team ID}" +xcodebuild \ + -workspace ios/expoiapexample.xcworkspace \ + -configuration Debug \ + -scheme expoiapexample \ + -destination "id=$IOS_UDID" \ + DEVELOPMENT_TEAM="$TEAM_ID" \ + -derivedDataPath build/DerivedData \ + -allowProvisioningUpdates \ + -allowProvisioningDeviceRegistration +# Build-only regression can stop here. +xcrun devicectl device install app \ + --device "$IOS_UDID" \ + build/DerivedData/Build/Products/Debug-iphoneos/expoiapexample.app +xcrun devicectl device process launch \ + --device "$IOS_UDID" \ + dev.hyo.openiap.expo.example +``` + +FireOS/Amazon Android path: + +```bash +cd libraries/expo-iap/example +EXPO_IAP_FIREOS=1 bunx expo prebuild --platform android --clean +cd android +./gradlew :app:assembleDebug +# Build-only regression can stop here. +: "${FIREOS_SERIAL:?Set FIREOS_SERIAL to the target FireOS device serial}" +adb -s "$FIREOS_SERIAL" install -r app/build/outputs/apk/debug/app-debug.apk +adb -s "$FIREOS_SERIAL" shell monkey -p dev.hyo.openiap.expo.example 1 +``` + +VegaOS/Kepler path: + +```bash +cd libraries/expo-iap/example +if [ -f "$HOME/vega/env" ]; then source "$HOME/vega/env"; fi +bun run build:vega:debug +bun run build:vega:release +# Build-only regression can stop here. +: "${VEGA_DEVICE_ID:?Set VEGA_DEVICE_ID to the target Vega device ID}" +VEGA_DEVICE_ID="$VEGA_DEVICE_ID" bun run run:vega:firetv +``` + +## React Native Checks + +Library: + +```bash +cd libraries/react-native-iap +yarn lint:tsc +yarn test:library --runInBand +``` + +Normal Android build and launch smoke: + +```bash +cd libraries/react-native-iap/example/android +./gradlew :app:assembleDebug +# Build-only regression can stop here. +: "${ANDROID_SERIAL:?Set ANDROID_SERIAL to the target Android device serial}" +adb -s "$ANDROID_SERIAL" install -r app/build/outputs/apk/debug/app-debug.apk +adb -s "$ANDROID_SERIAL" shell monkey -p dev.hyo.martie 1 +``` + +FireOS/Amazon Android build and launch smoke: + +```bash +cd libraries/react-native-iap/example/android +./gradlew :app:assembleDebug -PfireOsEnabled=true +# Build-only regression can stop here. +: "${FIREOS_SERIAL:?Set FIREOS_SERIAL to the target FireOS device serial}" +adb -s "$FIREOS_SERIAL" install -r app/build/outputs/apk/debug/app-debug.apk +adb -s "$FIREOS_SERIAL" shell monkey -p dev.hyo.martie 1 +``` + +Normal iOS physical-device build and launch smoke: + +```bash +cd libraries/react-native-iap/example +: "${IOS_UDID:?Set IOS_UDID to the target iOS device UDID}" +: "${TEAM_ID:?Set TEAM_ID to the Apple development team ID}" +xcodebuild \ + -workspace ios/example.xcworkspace \ + -configuration Debug \ + -scheme example \ + -destination "id=$IOS_UDID" \ + DEVELOPMENT_TEAM="$TEAM_ID" \ + -derivedDataPath build/DerivedData \ + -allowProvisioningUpdates \ + -allowProvisioningDeviceRegistration +# Build-only regression can stop here. +xcrun devicectl device install app \ + --device "$IOS_UDID" \ + build/DerivedData/Build/Products/Debug-iphoneos/example.app +xcrun devicectl device process launch \ + --device "$IOS_UDID" \ + dev.hyo.martie +``` + +VegaOS/Kepler path: + +```bash +cd libraries/react-native-iap/example +if [ -f "$HOME/vega/env" ]; then source "$HOME/vega/env"; fi +yarn build:vega:debug +yarn build:vega:release +# Build-only regression can stop here. +: "${VEGA_DEVICE_ID:?Set VEGA_DEVICE_ID to the target Vega device ID}" +VEGA_DEVICE_ID="$VEGA_DEVICE_ID" yarn run:vega:firetv +``` + +## Manual Store Flows + +Run these on each connected platform requested: + +- Android phone / Google Play: install the Play build, launch, fetch products, + buy a consumable, approve sandbox purchase if authorized, verify purchase + update and finish/consume behavior, then test restore/available purchases if + exposed. +- FireOS tablet / Amazon Appstore tester: install the Amazon/FireOS build, copy + tester catalog/config if needed, launch, fetch products, buy a consumable, + approve tester purchase if authorized, verify no crash on success, cancel, or + timeout/error paths. +- iPhone / StoreKit sandbox: install the iOS build, launch, fetch products, buy + a visible consumable or subscription, approve sandbox purchase if authorized, + verify transaction finish and restore behavior. +- VegaOS / Fire TV: build and install the VPK, copy Amazon tester catalog/config + with the project script, launch, fetch products, and run at least one purchase + attempt when device input and tester UI are available. + +If a product catalog or sandbox account is missing, stop that row and report it +as blocked with the exact missing prerequisite. + +## Final Report + +Use this compact matrix: + +```text +Platform/package | Target/device | Command/flow | Result | Notes +packages/google | local Gradle | ./gradlew ... | PASS | ... +packages/apple | local SwiftPM | swift test | PASS | ... +Expo Android | {serial} | assemble/install/purchase | PASS | ... +Expo FireOS | {serial} | prebuild/assemble/install/purchase | PASS | ... +Expo iOS | {UDID} | xcodebuild/purchase | PASS | ... +Expo Vega | {device id} | build debug/release/run | PASS | ... +RN Android | {serial} | assemble/install/purchase | PASS | ... +RN FireOS | {serial} | assemble/install/purchase | PASS | ... +RN iOS | {UDID} | xcodebuild/purchase | PASS | ... +RN Vega | {device id} | build debug/release/run | PASS | ... +``` + +Always list untested rows. Do not collapse "build passed" and "purchase flow +passed" into one claim unless both actually ran. diff --git a/.claude/commands/verify-all.md b/.claude/commands/verify-all.md index 8e1c7c4e..d32efb1d 100644 --- a/.claude/commands/verify-all.md +++ b/.claude/commands/verify-all.md @@ -87,8 +87,9 @@ All release workflows exist and have valid YAML: ls .github/workflows/release-{apple,google,react-native,expo,flutter,godot,kmp}.yml ``` -### 8. CLAUDE.md -- Root CLAUDE.md lists all 5 library CLAUDE.md files +### 8. Agent instructions +- Root AGENTS.md lists all framework library CLAUDE.md files +- Root CLAUDE.md and GEMINI.md are symlinks to AGENTS.md - `knowledge/internal/02-architecture.md` includes `libraries/` in structure - Auto-generated files list includes library types diff --git a/.codex/skills/openiap-workflows/SKILL.md b/.codex/skills/openiap-workflows/SKILL.md index a426eccd..145b0479 100644 --- a/.codex/skills/openiap-workflows/SKILL.md +++ b/.codex/skills/openiap-workflows/SKILL.md @@ -1,19 +1,19 @@ --- name: openiap-workflows -description: Use for OpenIAP monorepo work that should follow the repository's Claude slash-command workflows, including review-pr, audit-code, compile-knowledge, verify-all, resolve-issue, commit/push/PR, generated type sync, package-specific checks, GitHub review threads, and project conventions from CLAUDE.md/AGENTS.md. +description: Use for OpenIAP monorepo work that should follow the repository's Claude slash-command workflows, including review-pr, audit-code, compile-knowledge, verify-all, e2e-tests, resolve-issue, commit/push/PR, generated type sync, package-specific checks, GitHub review threads, and project conventions from AGENTS.md/CLAUDE.md/GEMINI.md. --- # OpenIAP Workflows Use this skill when the user asks Codex to perform an OpenIAP repo workflow that previously lived under `.claude/commands`, such as reviewing a PR, resolving an -issue, auditing code, compiling knowledge, verifying the monorepo, or committing -and opening a PR. +issue, auditing code, compiling knowledge, running device-backed E2E regression, +verifying the monorepo, or committing and opening a PR. ## Source Of Truth -Before changing code, read the root `AGENTS.md` or `CLAUDE.md`; they are linked -in this repo. Then read the relevant detailed files: +Before changing code, read the root `AGENTS.md`; `CLAUDE.md` and `GEMINI.md` +are symlinks to it in this repo. Then read the relevant detailed files: - Package and library rules: `knowledge/internal/*.md` - Package conventions: `packages/*/CONVENTION.md` @@ -37,6 +37,8 @@ natural-language requests, execute the matching workflow: - Resolve a GitHub issue: read `.claude/commands/resolve-issue.md`. - Verify all, health check, or pre-PR verification: read `.claude/commands/verify-all.md`. +- E2E tests, device regression, connected-device purchase flow checks, or + "e2e-tests": read `.claude/commands/e2e-tests.md`. - Commit, push, or create PR: read `.claude/commands/commit.md`. When a command file gives a sequence, follow it unless the user's newest @@ -45,9 +47,9 @@ instruction narrows the scope. ## Internal Workflow Change Guard Internal agent/workflow-only changes include `.claude/commands/`, -`.codex/skills/`, `AGENTS.md`, `CLAUDE.md`, and agent automation notes. Do not -create a branch, push, or open a PR for those changes unless the user explicitly -asks to publish, PR, or merge them. +`.codex/skills/`, `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, and agent automation +notes. Do not create a branch, push, or open a PR for those changes unless the +user explicitly asks to publish, PR, or merge them. If a user asks to update an internal workflow and does not explicitly ask for a PR, keep the change local and report the changed files. If a PR is already open diff --git a/AGENTS.md b/AGENTS.md deleted file mode 120000 index 681311eb..00000000 --- a/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -CLAUDE.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..f0596e64 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,196 @@ +# OpenIAP Monorepo - Agent Guidelines + +This document provides an overview for AI agents working across the OpenIAP monorepo. + +**All detailed rules are in the `knowledge/internal/` folder** - this is the Single Source of Truth (SSOT). + +## Quick Reference + +| Topic | File | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| Naming Conventions | [`knowledge/internal/01-naming-conventions.md`](knowledge/internal/01-naming-conventions.md) | +| Architecture | [`knowledge/internal/02-architecture.md`](knowledge/internal/02-architecture.md) | +| Coding Style | [`knowledge/internal/03-coding-style.md`](knowledge/internal/03-coding-style.md) | +| Platform Packages | [`knowledge/internal/04-platform-packages.md`](knowledge/internal/04-platform-packages.md) (run `bun audit:parity` before commits; pre-commit mirrors CI's SDK parity job) | +| Docs Patterns | [`knowledge/internal/05-docs-patterns.md`](knowledge/internal/05-docs-patterns.md) | +| Git & Deployment | [`knowledge/internal/06-git-deployment.md`](knowledge/internal/06-git-deployment.md) | +| Docs Consistency / SSOT | [`knowledge/internal/07-docs-consistency.md`](knowledge/internal/07-docs-consistency.md) (run `bun audit:docs` before pushing API/Type doc edits) | +| GV Cloud Workspaces | [`knowledge/internal/08-gv-cloud-workspaces.md`](knowledge/internal/08-gv-cloud-workspaces.md) | + +## Monorepo Structure + +```text +openiap/ +├── packages/ +│ ├── docs/ # Documentation site (React/Vite/Vercel) +│ ├── gql/ # GraphQL schema & type generation +│ ├── google/ # Android library +│ ├── apple/ # iOS/macOS library +│ └── kit/ # Hosted receipt-validation SaaS (Fly.io app) +├── libraries/ # Framework SDK implementations +│ ├── react-native-iap/ # React Native (npm) +│ ├── expo-iap/ # Expo (npm) +│ ├── flutter_inapp_purchase/ # Flutter (pub.dev) +│ ├── godot-iap/ # Godot 4.x (GitHub Release) +│ ├── kmp-iap/ # Kotlin Multiplatform (Maven Central) +│ └── maui-iap/ # .NET MAUI / C# (NuGet — scaffold) +├── knowledge/ # Shared knowledge base (SSOT) +│ ├── internal/ # Project philosophy (HIGHEST PRIORITY) +│ ├── external/ # External API reference +│ └── _claude-context/ # Compiled context for Claude Code +├── scripts/ # Monorepo-wide automation +└── .github/workflows/ # CI/CD workflows +``` + +## Required Pre-Work + +**CRITICAL**: Before writing or editing anything in a package or library: + +1. **Read the relevant knowledge files** from `knowledge/internal/` + - When the GraphQL schema adds or changes an API, follow the **SDK Parity Checklist** in [`knowledge/internal/04-platform-packages.md`](knowledge/internal/04-platform-packages.md#sdk-parity-checklist-critical--prevents-declared-but-not-implemented) to avoid phantom interfaces (declared in types but never wired end-to-end — the class of bug behind GitHub issue #104). +2. **Check the package-specific CONVENTION.md**: + - [`packages/gql/CONVENTION.md`](packages/gql/CONVENTION.md) + - [`packages/google/CONVENTION.md`](packages/google/CONVENTION.md) + - [`packages/apple/CONVENTION.md`](packages/apple/CONVENTION.md) + - [`packages/kit/CONVENTION.md`](packages/kit/CONVENTION.md) — kit is a deployable SaaS (not a library); has its own Convex schema and isn't part of the GQL type-sync chain +3. **For framework libraries, read the library-specific CLAUDE.md**: + - [`libraries/react-native-iap/CLAUDE.md`](libraries/react-native-iap/CLAUDE.md) — Yarn 3, Nitro Modules, useIAP hook semantics, error handling + - [`libraries/expo-iap/CLAUDE.md`](libraries/expo-iap/CLAUDE.md) — Bun, Expo Modules, iOS podspec 13.4 workaround, tvOS 16.0 requirement + - [`libraries/flutter_inapp_purchase/CLAUDE.md`](libraries/flutter_inapp_purchase/CLAUDE.md) — Flutter/Dart, generated types.dart, fetchProducts generic API + - [`libraries/godot-iap/CLAUDE.md`](libraries/godot-iap/CLAUDE.md) — GDScript conventions, GDExtension (iOS), AAR plugin (Android) + - [`libraries/kmp-iap/CLAUDE.md`](libraries/kmp-iap/CLAUDE.md) — Kotlin Multiplatform, Flow-based API, CocoaPods iOS integration + - [`libraries/maui-iap/CLAUDE.md`](libraries/maui-iap/CLAUDE.md) — .NET MAUI / C# 12, generated Types.cs, Xamarin binding plan + +## Key Rules Summary + +### Platform Function Naming + +- **iOS functions**: Must end with `IOS` suffix (e.g., `syncIOS`, `getStorefrontIOS`) +- **Android functions in packages/google**: NO `Android` suffix (it's Android-only) +- **Cross-platform functions**: NO suffix + +### Auto-Generated Files (DO NOT EDIT) + +- `packages/gql/src/generated/*` - All generated type files (SSOT) +- `packages/apple/Sources/Models/Types.swift` - Synced from GQL +- `packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt` - Synced from GQL +- `libraries/react-native-iap/src/types.ts` - Synced from GQL +- `libraries/expo-iap/src/types.ts` - Synced from GQL +- `libraries/flutter_inapp_purchase/lib/types.dart` - Synced from GQL +- `libraries/godot-iap/addons/godot-iap/types.gd` - Synced from GQL +- `libraries/maui-iap/src/OpenIap.Maui/Types.cs` - Synced from GQL +- `openiap-versions.json` - Managed by CI/CD workflows only; tracks only `spec`, `google`, and `apple` + +Framework library package versions (React Native, Expo, Flutter, Godot, KMP, +MAUI) live in their own package metadata / release workflows. Do not add +framework-library version keys to `openiap-versions.json`. + +When writing release notes or `Package Releases` lists, verify framework +versions from each library's metadata and verify published links with GitHub +release tags. Do not infer framework versions from `openiap-versions.json` or +copy nearby release blocks without checking the actual package/tag. + +Regenerate and sync types: + +```bash +cd packages/gql && bun run generate # Generate types from GraphQL schema +cd ../.. && ./scripts/sync-versions.sh # Sync to all packages and libraries +``` + +### GQL Code Generation System + +The type generation uses an **IR-based (Intermediate Representation)** architecture: + +```text +GraphQL Schema → Parser → IR → Language Plugins → Generated Code + ↓ + codegen/core/ codegen/plugins/ + ├── types.ts ├── swift.ts + ├── parser.ts ├── kotlin.ts + └── transformer.ts├── dart.ts + ├── gdscript.ts + └── csharp.ts +``` + +**Language plugins handle:** + +- **Swift**: Codable protocol, ErrorCode custom initializer, platform defaults +- **Kotlin**: sealed interface, fromJson/toJson with nullable patterns +- **Dart**: sealed class, factory constructors, extends/implements +- **GDScript**: \_init() pattern, Variant type for unions +- **C#**: sealed records, per-enum JsonConverter, [JsonPolymorphic] unions + +### Git Commit Format + +- With tag: `feat: add new feature` (lowercase after tag) +- Without tag: `Add new feature` (uppercase first letter) + +## Using Claude Code with Context + +```bash +cd scripts/agent + +# Compile for AI assistants (no Ollama required) +bun run compile:ai + +# Or compile for both Claude Code + Local RAG +bun run compile + +# Use with Claude Code +claude --context knowledge/_claude-context/context.md +``` + +## Codex Compatibility + +`AGENTS.md` is the root project instruction SSOT. `CLAUDE.md` and `GEMINI.md` +are symlinks to `AGENTS.md`, so Claude Code, Gemini, and Codex read the same +root instructions. The `.claude/commands/` files remain the workflow SSOT for +slash-command-style tasks. + +Codex supports Skills through `SKILL.md` folders. This repo provides a +Codex-compatible local skill at `.codex/skills/openiap-workflows/` that maps the +Claude slash-command workflows to Codex natural-language requests. + +Install it into your local Codex home when needed: + +```bash +./.codex/scripts/install-skills.sh +``` + +After installation, ask Codex normally (for example, "review PR 65" or +"resolve issue 88"), or explicitly mention `$openiap-workflows`. + +## Available Skills (Slash Commands / Codex Workflows) + +| Skill | Description | Usage | +| -------------------- | -------------------------------------------------- | ------------------------------------- | +| `/review-pr` | Review PR comments, fix issues, resolve threads | `/review-pr 65` or `/review-pr ` | +| `/audit-code` | Audit code against knowledge rules and latest APIs | `/audit-code` | +| `/compile-knowledge` | Compile knowledge base for Claude context | `/compile-knowledge` | +| `/resolve-issue` | Analyze an issue, label it, and fix/comment | `/resolve-issue 88` | +| `/verify-all` | Run the full monorepo health check | `/verify-all` | +| `/e2e-tests` | Run device-backed OpenIAP regression tests | `/e2e-tests PR 162` | +| `/commit` | Branch, commit, push, and optionally create PR | `/commit --all --pr` | + +### /review-pr Workflow + +1. Fetches unresolved PR review threads +2. For each comment: + - **Valid issue** → Fix code + - **Invalid/wrong** → Reply with explanation (don't resolve) +3. **Run lint, typecheck, tests** (BEFORE commit) +4. If all pass → Commit and push +5. Resolve fixed threads + +## For More Details + +All comprehensive rules are documented in [`knowledge/internal/`](knowledge/internal/): + +1. **01-naming-conventions.md** - Function naming, prefixes, file naming, URL anchors +2. **02-architecture.md** - Monorepo structure, module patterns, async patterns +3. **03-coding-style.md** - TypeScript/Swift/Kotlin style rules, error handling +4. **04-platform-packages.md** - Apple/Google/GQL/Docs package workflows +5. **05-docs-patterns.md** - React modal patterns, component organization +6. **06-git-deployment.md** - Commit format, deployment workflows +7. **07-docs-consistency.md** - Docs/API/type consistency audits +8. **08-gv-cloud-workspaces.md** - Safe TabTabTab `gv` cloud workspace policy diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 4e47d82a..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,194 +0,0 @@ -# OpenIAP Monorepo - Agent Guidelines - -This document provides an overview for AI agents working across the OpenIAP monorepo. - -**All detailed rules are in the `knowledge/internal/` folder** - this is the Single Source of Truth (SSOT). - -## Quick Reference - -| Topic | File | -| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| Naming Conventions | [`knowledge/internal/01-naming-conventions.md`](knowledge/internal/01-naming-conventions.md) | -| Architecture | [`knowledge/internal/02-architecture.md`](knowledge/internal/02-architecture.md) | -| Coding Style | [`knowledge/internal/03-coding-style.md`](knowledge/internal/03-coding-style.md) | -| Platform Packages | [`knowledge/internal/04-platform-packages.md`](knowledge/internal/04-platform-packages.md) (run `bun audit:parity` before commits; pre-commit mirrors CI's SDK parity job) | -| Docs Patterns | [`knowledge/internal/05-docs-patterns.md`](knowledge/internal/05-docs-patterns.md) | -| Git & Deployment | [`knowledge/internal/06-git-deployment.md`](knowledge/internal/06-git-deployment.md) | -| Docs Consistency / SSOT | [`knowledge/internal/07-docs-consistency.md`](knowledge/internal/07-docs-consistency.md) (run `bun audit:docs` before pushing API/Type doc edits) | -| GV Cloud Workspaces | [`knowledge/internal/08-gv-cloud-workspaces.md`](knowledge/internal/08-gv-cloud-workspaces.md) | - -## Monorepo Structure - -```text -openiap/ -├── packages/ -│ ├── docs/ # Documentation site (React/Vite/Vercel) -│ ├── gql/ # GraphQL schema & type generation -│ ├── google/ # Android library -│ ├── apple/ # iOS/macOS library -│ └── kit/ # Hosted receipt-validation SaaS (Fly.io app) -├── libraries/ # Framework SDK implementations -│ ├── react-native-iap/ # React Native (npm) -│ ├── expo-iap/ # Expo (npm) -│ ├── flutter_inapp_purchase/ # Flutter (pub.dev) -│ ├── godot-iap/ # Godot 4.x (GitHub Release) -│ ├── kmp-iap/ # Kotlin Multiplatform (Maven Central) -│ └── maui-iap/ # .NET MAUI / C# (NuGet — scaffold) -├── knowledge/ # Shared knowledge base (SSOT) -│ ├── internal/ # Project philosophy (HIGHEST PRIORITY) -│ ├── external/ # External API reference -│ └── _claude-context/ # Compiled context for Claude Code -├── scripts/ # Monorepo-wide automation -└── .github/workflows/ # CI/CD workflows -``` - -## Required Pre-Work - -**CRITICAL**: Before writing or editing anything in a package or library: - -1. **Read the relevant knowledge files** from `knowledge/internal/` - - When the GraphQL schema adds or changes an API, follow the **SDK Parity Checklist** in [`knowledge/internal/04-platform-packages.md`](knowledge/internal/04-platform-packages.md#sdk-parity-checklist-critical--prevents-declared-but-not-implemented) to avoid phantom interfaces (declared in types but never wired end-to-end — the class of bug behind GitHub issue #104). -2. **Check the package-specific CONVENTION.md**: - - [`packages/gql/CONVENTION.md`](packages/gql/CONVENTION.md) - - [`packages/google/CONVENTION.md`](packages/google/CONVENTION.md) - - [`packages/apple/CONVENTION.md`](packages/apple/CONVENTION.md) - - [`packages/kit/CONVENTION.md`](packages/kit/CONVENTION.md) — kit is a deployable SaaS (not a library); has its own Convex schema and isn't part of the GQL type-sync chain -3. **For framework libraries, read the library-specific CLAUDE.md**: - - [`libraries/react-native-iap/CLAUDE.md`](libraries/react-native-iap/CLAUDE.md) — Yarn 3, Nitro Modules, useIAP hook semantics, error handling - - [`libraries/expo-iap/CLAUDE.md`](libraries/expo-iap/CLAUDE.md) — Bun, Expo Modules, iOS podspec 13.4 workaround, tvOS 16.0 requirement - - [`libraries/flutter_inapp_purchase/CLAUDE.md`](libraries/flutter_inapp_purchase/CLAUDE.md) — Flutter/Dart, generated types.dart, fetchProducts generic API - - [`libraries/godot-iap/CLAUDE.md`](libraries/godot-iap/CLAUDE.md) — GDScript conventions, GDExtension (iOS), AAR plugin (Android) - - [`libraries/kmp-iap/CLAUDE.md`](libraries/kmp-iap/CLAUDE.md) — Kotlin Multiplatform, Flow-based API, CocoaPods iOS integration - - [`libraries/maui-iap/CLAUDE.md`](libraries/maui-iap/CLAUDE.md) — .NET MAUI / C# 12, generated Types.cs, Xamarin binding plan - -## Key Rules Summary - -### Platform Function Naming - -- **iOS functions**: Must end with `IOS` suffix (e.g., `syncIOS`, `getStorefrontIOS`) -- **Android functions in packages/google**: NO `Android` suffix (it's Android-only) -- **Cross-platform functions**: NO suffix - -### Auto-Generated Files (DO NOT EDIT) - -- `packages/gql/src/generated/*` - All generated type files (SSOT) -- `packages/apple/Sources/Models/Types.swift` - Synced from GQL -- `packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt` - Synced from GQL -- `libraries/react-native-iap/src/types.ts` - Synced from GQL -- `libraries/expo-iap/src/types.ts` - Synced from GQL -- `libraries/flutter_inapp_purchase/lib/types.dart` - Synced from GQL -- `libraries/godot-iap/addons/godot-iap/types.gd` - Synced from GQL -- `libraries/maui-iap/src/OpenIap.Maui/Types.cs` - Synced from GQL -- `openiap-versions.json` - Managed by CI/CD workflows only; tracks only `spec`, `google`, and `apple` - -Framework library package versions (React Native, Expo, Flutter, Godot, KMP, -MAUI) live in their own package metadata / release workflows. Do not add -framework-library version keys to `openiap-versions.json`. - -When writing release notes or `Package Releases` lists, verify framework -versions from each library's metadata and verify published links with GitHub -release tags. Do not infer framework versions from `openiap-versions.json` or -copy nearby release blocks without checking the actual package/tag. - -Regenerate and sync types: - -```bash -cd packages/gql && bun run generate # Generate types from GraphQL schema -cd ../.. && ./scripts/sync-versions.sh # Sync to all packages and libraries -``` - -### GQL Code Generation System - -The type generation uses an **IR-based (Intermediate Representation)** architecture: - -```text -GraphQL Schema → Parser → IR → Language Plugins → Generated Code - ↓ - codegen/core/ codegen/plugins/ - ├── types.ts ├── swift.ts - ├── parser.ts ├── kotlin.ts - └── transformer.ts├── dart.ts - ├── gdscript.ts - └── csharp.ts -``` - -**Language plugins handle:** - -- **Swift**: Codable protocol, ErrorCode custom initializer, platform defaults -- **Kotlin**: sealed interface, fromJson/toJson with nullable patterns -- **Dart**: sealed class, factory constructors, extends/implements -- **GDScript**: \_init() pattern, Variant type for unions -- **C#**: sealed records, per-enum JsonConverter, [JsonPolymorphic] unions - -### Git Commit Format - -- With tag: `feat: add new feature` (lowercase after tag) -- Without tag: `Add new feature` (uppercase first letter) - -## Using Claude Code with Context - -```bash -cd scripts/agent - -# Compile for AI assistants (no Ollama required) -bun run compile:ai - -# Or compile for both Claude Code + Local RAG -bun run compile - -# Use with Claude Code -claude --context knowledge/_claude-context/context.md -``` - -## Codex Compatibility - -`AGENTS.md` is a symlink to this file, so Codex reads the same root project -instructions as Claude Code. The `.claude/commands/` files remain the workflow -SSOT for slash-command-style tasks. - -Codex supports Skills through `SKILL.md` folders. This repo provides a -Codex-compatible local skill at `.codex/skills/openiap-workflows/` that maps the -Claude slash-command workflows to Codex natural-language requests. - -Install it into your local Codex home when needed: - -```bash -./.codex/scripts/install-skills.sh -``` - -After installation, ask Codex normally (for example, "review PR 65" or -"resolve issue 88"), or explicitly mention `$openiap-workflows`. - -## Available Skills (Slash Commands / Codex Workflows) - -| Skill | Description | Usage | -| -------------------- | -------------------------------------------------- | ------------------------------------- | -| `/review-pr` | Review PR comments, fix issues, resolve threads | `/review-pr 65` or `/review-pr ` | -| `/audit-code` | Audit code against knowledge rules and latest APIs | `/audit-code` | -| `/compile-knowledge` | Compile knowledge base for Claude context | `/compile-knowledge` | -| `/resolve-issue` | Analyze an issue, label it, and fix/comment | `/resolve-issue 88` | -| `/verify-all` | Run the full monorepo health check | `/verify-all` | -| `/commit` | Branch, commit, push, and optionally create PR | `/commit --all --pr` | - -### /review-pr Workflow - -1. Fetches unresolved PR review threads -2. For each comment: - - **Valid issue** → Fix code - - **Invalid/wrong** → Reply with explanation (don't resolve) -3. **Run lint, typecheck, tests** (BEFORE commit) -4. If all pass → Commit and push -5. Resolve fixed threads - -## For More Details - -All comprehensive rules are documented in [`knowledge/internal/`](knowledge/internal/): - -1. **01-naming-conventions.md** - Function naming, prefixes, file naming, URL anchors -2. **02-architecture.md** - Monorepo structure, module patterns, async patterns -3. **03-coding-style.md** - TypeScript/Swift/Kotlin style rules, error handling -4. **04-platform-packages.md** - Apple/Google/GQL/Docs package workflows -5. **05-docs-patterns.md** - React modal patterns, component organization -6. **06-git-deployment.md** - Commit format, deployment workflows -7. **07-docs-consistency.md** - Docs/API/type consistency audits -8. **08-gv-cloud-workspaces.md** - Safe TabTabTab `gv` cloud workspace policy diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md index 681311eb..47dc3e3d 120000 --- a/GEMINI.md +++ b/GEMINI.md @@ -1 +1 @@ -CLAUDE.md \ No newline at end of file +AGENTS.md \ No newline at end of file From df0a65bd5db730f29177a8935b05a576dad699cf Mon Sep 17 00:00:00 2001 From: Hyo Date: Fri, 12 Jun 2026 01:19:20 +0900 Subject: [PATCH 49/51] test(e2e): expand OpenIAP device regression matrix Document full e2e coverage across native packages and framework examples, including build-only Horizon and Onside rows plus blocked KMP/MAUI FireOS/Horizon rows. Make Expo example Horizon and Onside toggles env-driven, and add resilient Vega install fallback for RN/Expo example run scripts. --- .claude/commands/e2e-tests.md | 410 +++++++++++++++++- .codex/skills/openiap-workflows/SKILL.md | 4 + libraries/expo-iap/example/app.config.ts | 5 +- .../example/scripts/run-vega-firetv.mjs | 79 +++- .../example/scripts/run-vega-firetv.mjs | 79 +++- 5 files changed, 557 insertions(+), 20 deletions(-) diff --git a/.claude/commands/e2e-tests.md b/.claude/commands/e2e-tests.md index ba3e98d3..6425ddff 100644 --- a/.claude/commands/e2e-tests.md +++ b/.claude/commands/e2e-tests.md @@ -9,9 +9,9 @@ Use the narrowest scope that satisfies the request, but broaden when native build configuration, dependency placement, config plugins, package manifests, or store-specific behavior changed. -- **Full PR regression**: package checks, Expo checks, React Native checks, - Android/FireOS/iOS builds, Vega debug/release builds, and manual purchase - smoke flows on connected devices. +- **Full PR regression**: package checks, native package flavor checks, and + every supported library row in the matrix below. Do not stop after + React Native and Expo unless the user explicitly narrows the scope. - **Build-only regression**: all relevant compile/build commands, no purchase dialogs. Clearly report that purchase flows were not exercised. - **Store-flow regression**: build/install each requested target, then verify @@ -21,6 +21,34 @@ or store-specific behavior changed. builds first, then FireOS/Amazon and Vega builds. This catches accidental Kepler dependency leakage. +## Full Regression Matrix + +When `e2e-tests` is requested without a narrower scope, run every applicable +row and report every unavailable row as `BLOCKED` or `UNSUPPORTED` with the +exact missing command, tool, device, or store prerequisite. + +| Target | Android / Play | FireOS / Amazon | Horizon | iOS | VegaOS | Onside | +| ------ | -------------- | --------------- | ------- | --- | ------ | ------ | +| `packages/google` native | build + tests | build + tests | build-only | n/a | n/a | n/a | +| `packages/apple` native | n/a | n/a | n/a | build + tests | n/a | n/a | +| `react-native-iap` | build + device flow | build + device flow | build-only | build + device flow | RN only | n/a | +| `expo-iap` | build + device flow | build + device flow | build-only | build + device flow | Expo only | build-only | +| `flutter_inapp_purchase` | build + device flow | build + device flow | build-only | build + device flow | n/a | n/a | +| `kmp-iap` | build + device flow | required row; currently blocked unless a FireOS flavor is wired | required row; currently blocked unless a Horizon flavor is wired | build + device flow | n/a | n/a | +| `maui-iap` | build + device flow | required row; currently blocked unless an Amazon binding flavor is wired | required row; currently blocked unless a Horizon binding flavor is wired | build + device flow | n/a | n/a | +| `godot-iap` | build + device flow | n/a | n/a | build + device flow | n/a | n/a | + +Notes: + +- VegaOS is required only for `react-native-iap` and `expo-iap`. +- Godot is required only on Android and iOS. +- Horizon is build-only unless the user explicitly provides a Horizon device and + the library has a runnable Horizon example. +- Onside is Expo-only and build-only; do not require Onside purchase approval. +- KMP and MAUI must still appear in the final report for FireOS/Horizon. If the + repo has no flavor switch for those libraries, mark the row blocked instead of + treating the Play Android build as coverage. + ## Rules - Check `git status --short --branch` before changes. Do not revert user @@ -35,6 +63,12 @@ or store-specific behavior changed. - Treat missing store account, catalog, entitlement, tester app, or device connectivity as blocked, not passed. - For VegaOS, source `~/vega/env` before invoking `vega` when present. +- For VegaOS, `vega exec vda devices -l` is transport visibility only. Treat + `kepler device list` or `vega device list` as the install/launch source of + truth because plain VDA can also list Android and FireOS devices. +- For VegaOS, do not assume Android `screencap`, `input`, or `scrcpy` works. + Prefer `kepler device launch-app`, `is-app-running`, `running-apps`, and logs; + require user/capture-device visual confirmation for screen-only purchase UI. - For Expo Vega, verify normal Expo iOS/Android manifests do not require Vega-only dependencies unless `amazon.vegaOS=true` or a generated Vega temp target requires them. @@ -63,7 +97,12 @@ fi if [ -f "$HOME/vega/env" ]; then source "$HOME/vega/env"; fi if command -v vega >/dev/null 2>&1; then vega -v --json - vega exec vda devices + vega exec vda devices -l + vega device list || true + if command -v kepler >/dev/null 2>&1; then + kepler --version + kepler device list || true + fi else echo "vega: not found" fi @@ -72,6 +111,21 @@ bun -v PATH="$HOME/.bun/bin:$PATH" bun -v (cd libraries/react-native-iap && yarn -v) java -version +if command -v flutter >/dev/null 2>&1; then + flutter --version +else + echo "flutter: not found" +fi +if command -v dotnet >/dev/null 2>&1; then + dotnet --info +else + echo "dotnet: not found" +fi +if command -v godot >/dev/null 2>&1; then + godot --version +else + echo "godot: not found" +fi if command -v xcodebuild >/dev/null 2>&1; then xcodebuild -version else @@ -85,6 +139,88 @@ If the repo expects the bundled Bun version, prefer this from the repo root: PATH="$HOME/.bun/bin:$PATH" bun run audit:parity ``` +## VegaOS Device Checks + +Run this before any Vega build/install claim: + +```bash +if [ -f "$HOME/vega/env" ]; then source "$HOME/vega/env"; fi +vega exec vda start-server +vega exec vda devices -l +kepler device list || vega device list +: "${VEGA_DEVICE_ID:?Set VEGA_DEVICE_ID from kepler/vega device list}" +kepler device info -d "$VEGA_DEVICE_ID" +kepler device installed-packages -d "$VEGA_DEVICE_ID" | grep -E \ + 'dev\.hyo\.openiap|dev\.hyo\.martie|com\.amazonappstore\.iap\.tester|com\.amazon\.iap\.core' +kepler device installed-apps -d "$VEGA_DEVICE_ID" | grep -E \ + 'dev\.hyo\.openiap|dev\.hyo\.martie|com\.amazonappstore\.iap\.tester|com\.amazon\.iap\.tester' +``` + +USB and TCP/IP setup: + +```bash +# USB mode should make the Fire TV / VegaOS device appear in both commands. +vega exec vda devices -l +kepler device list || vega device list + +# TCP/IP mode uses the device IP as the VDA device id after connect. +# Use -s only when multiple VDA devices are attached over USB. +vega exec vda -s "$VEGA_DEVICE_ID" tcpip 5555 +vega exec vda connect "$VEGA_DEVICE_HOST:5555" +vega exec vda devices -l +kepler device list || vega device list +``` + +VegaOS screen and input caveat: + +```bash +# These may fail on VegaOS. Failure does not block build/install/run checks. +vega exec vda -s "$VEGA_DEVICE_ID" shell screencap -p > /tmp/vega.png || true +scrcpy -s "$VEGA_DEVICE_ID" --no-audio --time-limit=5 || true +``` + +If screen capture is unavailable, record app state with: + +```bash +kepler device running-apps -d "$VEGA_DEVICE_ID" +kepler device is-app-running -d "$VEGA_DEVICE_ID" -a dev.hyo.openiap.expo.example.main +kepler device is-app-running -d "$VEGA_DEVICE_ID" -a dev.hyo.openiap.rniap.example.main +kepler device start-log-stream -d "$VEGA_DEVICE_ID" +``` + +If Vega install hangs or fails, keep build and install results separate: + +```bash +kepler device install-app \ + -d "$VEGA_DEVICE_ID" \ + --packagePath build/armv7-debug/expoiapvegaexample_armv7.vpkg +kepler device installed-packages -d "$VEGA_DEVICE_ID" | grep 'dev.hyo.openiap' +tail -n 120 "$HOME/vega/sdk/vega-sdk/main/0.22.6759/logs/error-$(date +%F).log" +``` + +If `install-app` or synchronous `vpm install` returns `Internal Error` while +`vpm print --manifest` still works, inspect and cancel only the stale OpenIAP +install tokens, then retry with async high-priority install: + +```bash +vega exec vda -s "$VEGA_DEVICE_ID" shell vpm query-installs | grep openiap +vega exec vda -s "$VEGA_DEVICE_ID" shell vpm cancel-download "$OPENIAP_INSTALL_TOKEN" --timeout=5 +vega exec vda -s "$VEGA_DEVICE_ID" push \ + build/armv7-debug/expoiapvegaexample_armv7.vpkg \ + /tmp/expoiapvegaexample_armv7.vpkg +vega exec vda -s "$VEGA_DEVICE_ID" shell vpm install-async \ + /tmp/expoiapvegaexample_armv7.vpkg \ + --token=openiap_expo_$(date +%s) \ + --timeout=60 \ + --high-priority \ + --force \ + --update-max-timeout=5 \ + --terminate-on-max-timeout +``` + +Report `vpm install` internal errors, App Tester scratch permission errors, or +missing package rows as install failures, not build failures. + ## Package-Level Checks Google Android package: @@ -95,6 +231,7 @@ cd packages/google :openiap:compileHorizonDebugKotlin \ :openiap:compileAmazonDebugKotlin \ :Example:compileAmazonDebugKotlin \ + :Example:compileHorizonDebugKotlin \ :Example:compilePlayDebugKotlin \ :openiap:test ``` @@ -103,7 +240,11 @@ Apple package: ```bash cd packages/apple +swift build swift test +# Required when iOS native packaging/bindings changed or MAUI/Godot iOS rows +# consume the xcframework. +bash scripts/build-xcframework.sh ``` Docs and parity when docs, API surface, generated types, examples, or package @@ -179,6 +320,28 @@ adb -s "$FIREOS_SERIAL" install -r app/build/outputs/apk/debug/app-debug.apk adb -s "$FIREOS_SERIAL" shell monkey -p dev.hyo.openiap.expo.example 1 ``` +Horizon Android build-only path: + +```bash +cd libraries/expo-iap/example +EXPO_IAP_HORIZON=1 bunx expo prebuild --platform android --clean +cd android +./gradlew :app:assembleDebug +``` + +Onside iOS build-only path: + +```bash +cd libraries/expo-iap/example +EXPO_IAP_ONSIDE=1 bunx expo prebuild --platform ios --clean +xcodebuild \ + -workspace ios/expoiapexample.xcworkspace \ + -configuration Debug \ + -scheme expoiapexample \ + -destination "generic/platform=iOS" \ + CODE_SIGNING_ALLOWED=NO +``` + VegaOS/Kepler path: ```bash @@ -189,6 +352,9 @@ bun run build:vega:release # Build-only regression can stop here. : "${VEGA_DEVICE_ID:?Set VEGA_DEVICE_ID to the target Vega device ID}" VEGA_DEVICE_ID="$VEGA_DEVICE_ID" bun run run:vega:firetv +kepler device is-app-running \ + -d "$VEGA_DEVICE_ID" \ + -a dev.hyo.openiap.expo.example.main ``` ## React Native Checks @@ -223,6 +389,13 @@ adb -s "$FIREOS_SERIAL" install -r app/build/outputs/apk/debug/app-debug.apk adb -s "$FIREOS_SERIAL" shell monkey -p dev.hyo.martie 1 ``` +Horizon Android build-only path: + +```bash +cd libraries/react-native-iap/example/android +./gradlew :app:assembleDebug -PhorizonEnabled=true +``` + Normal iOS physical-device build and launch smoke: ```bash @@ -257,6 +430,185 @@ yarn build:vega:release # Build-only regression can stop here. : "${VEGA_DEVICE_ID:?Set VEGA_DEVICE_ID to the target Vega device ID}" VEGA_DEVICE_ID="$VEGA_DEVICE_ID" yarn run:vega:firetv +kepler device is-app-running \ + -d "$VEGA_DEVICE_ID" \ + -a dev.hyo.openiap.rniap.example.main +``` + +## Flutter Checks + +Library and Dart tests: + +```bash +cd libraries/flutter_inapp_purchase +flutter pub get +git ls-files '*.dart' | grep -v '^lib/types.dart$' | \ + xargs dart format --page-width 80 --output=none --set-exit-if-changed +flutter analyze +flutter test +``` + +Normal Android build and launch smoke: + +```bash +cd libraries/flutter_inapp_purchase/example +flutter pub get +flutter build apk --debug +# Build-only regression can stop here. +: "${ANDROID_SERIAL:?Set ANDROID_SERIAL to the target Android device serial}" +adb -s "$ANDROID_SERIAL" install -r build/app/outputs/flutter-apk/app-debug.apk +adb -s "$ANDROID_SERIAL" shell monkey -p dev.hyo.martie 1 +``` + +FireOS/Amazon Android build and launch smoke: + +```bash +cd libraries/flutter_inapp_purchase/example/android +./gradlew :app:assembleDebug -PfireOsEnabled=true +# Build-only regression can stop here. +: "${FIREOS_SERIAL:?Set FIREOS_SERIAL to the target FireOS device serial}" +adb -s "$FIREOS_SERIAL" install -r app/build/outputs/apk/debug/app-debug.apk +adb -s "$FIREOS_SERIAL" shell monkey -p dev.hyo.martie 1 +``` + +Horizon Android build-only path: + +```bash +cd libraries/flutter_inapp_purchase/example/android +./gradlew :app:assembleDebug -PhorizonEnabled=true +``` + +iOS physical-device build and launch smoke: + +```bash +cd libraries/flutter_inapp_purchase/example +flutter build ios --debug --no-codesign +# Build-only regression can stop here. +: "${IOS_UDID:?Set IOS_UDID to the target iOS device UDID}" +flutter run -d "$IOS_UDID" --debug +``` + +## KMP Checks + +Library build and tests: + +```bash +cd libraries/kmp-iap +./gradlew :library:build :library:test :library:podspec :library:generateDummyFramework +``` + +Normal Android build and launch smoke: + +```bash +cd libraries/kmp-iap/example +./gradlew :composeApp:assembleDebug +# Build-only regression can stop here. +: "${ANDROID_SERIAL:?Set ANDROID_SERIAL to the target Android device serial}" +adb -s "$ANDROID_SERIAL" install -r composeApp/build/outputs/apk/debug/composeApp-debug.apk +adb -s "$ANDROID_SERIAL" shell monkey -p dev.hyo.martie 1 +``` + +iOS physical-device build and launch smoke: + +```bash +cd libraries/kmp-iap/example/iosApp +: "${IOS_UDID:?Set IOS_UDID to the target iOS device UDID}" +: "${TEAM_ID:?Set TEAM_ID to the Apple development team ID}" +xcodebuild \ + -project iosApp.xcodeproj \ + -configuration Debug \ + -scheme iosApp \ + -destination "id=$IOS_UDID" \ + DEVELOPMENT_TEAM="$TEAM_ID" \ + -derivedDataPath build/DerivedData \ + -allowProvisioningUpdates \ + -allowProvisioningDeviceRegistration +``` + +FireOS/Amazon and Horizon rows: + +```text +KMP currently has no FireOS/Amazon or Horizon flavor switch in +libraries/kmp-iap/library/build.gradle.kts or example/composeApp/build.gradle.kts. +Report these rows as BLOCKED until the flavor is wired; do not count the normal +Android build as FireOS or Horizon coverage. +``` + +## MAUI Checks + +Library build and native binding prerequisites: + +```bash +cd packages/google +./gradlew :openiap:assemblePlayRelease +cd ../../libraries/maui-iap/android +../../../packages/google/gradlew :openiap:assembleRelease +cd ../../.. +bash packages/apple/scripts/build-xcframework.sh +cd libraries/maui-iap +dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj -f net9.0 +dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj -f net9.0-android +dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj -f net9.0-ios +``` + +Normal Android build and launch smoke: + +```bash +cd libraries/maui-iap/example/OpenIap.Maui.Example +dotnet build -t:Run -f net9.0-android +``` + +iOS physical-device build and launch smoke: + +```bash +cd libraries/maui-iap/example/OpenIap.Maui.Example +: "${IOS_UDID:?Set IOS_UDID to the target iOS device UDID}" +dotnet build -t:Run -f net9.0-ios -p:_DeviceName="$IOS_UDID" +``` + +FireOS/Amazon and Horizon rows: + +```text +MAUI currently builds its Android binding against the Play flavor. It has no +Amazon or Horizon binding flavor switch. Report FireOS/Horizon rows as BLOCKED +until libraries/maui-iap/android and the binding csproj expose those variants. +``` + +## Godot Checks + +Godot is Android/iOS only for this e2e matrix. + +Android build and launch smoke: + +```bash +cd libraries/godot-iap +make setup +make android +# Build-only regression can stop here. +: "${ANDROID_SERIAL:?Set ANDROID_SERIAL to the target Android device serial}" +make export-android +adb -s "$ANDROID_SERIAL" install -r Example/android/Martie.apk +adb -s "$ANDROID_SERIAL" shell monkey -p dev.hyo.martie 1 +``` + +iOS build and launch smoke: + +```bash +cd libraries/godot-iap +make setup +make ios +make export-ios +: "${IOS_UDID:?Set IOS_UDID to the target iOS device UDID}" +: "${TEAM_ID:?Set TEAM_ID to the Apple development team ID}" +xcodebuild \ + -project Example/ios/Martie.xcodeproj \ + -configuration Debug \ + -scheme Martie \ + -destination "id=$IOS_UDID" \ + DEVELOPMENT_TEAM="$TEAM_ID" \ + -derivedDataPath Example/ios/DerivedData \ + -allowProvisioningUpdates \ + -allowProvisioningDeviceRegistration ``` ## Manual Store Flows @@ -277,6 +629,12 @@ Run these on each connected platform requested: - VegaOS / Fire TV: build and install the VPK, copy Amazon tester catalog/config with the project script, launch, fetch products, and run at least one purchase attempt when device input and tester UI are available. +- Horizon: build-only unless a runnable Horizon device and store prerequisites + are explicitly available. If it is build-only, do not claim purchase flow + coverage. +- Onside: Expo iOS build-only. Verify the generated Podfile and iOS project + include the Onside module when `EXPO_IAP_ONSIDE=1`; do not claim purchase + flow coverage unless the Onside app/account/test flow was actually exercised. If a product catalog or sandbox account is missing, stop that row and report it as blocked with the exact missing prerequisite. @@ -286,18 +644,38 @@ as blocked with the exact missing prerequisite. Use this compact matrix: ```text -Platform/package | Target/device | Command/flow | Result | Notes -packages/google | local Gradle | ./gradlew ... | PASS | ... -packages/apple | local SwiftPM | swift test | PASS | ... -Expo Android | {serial} | assemble/install/purchase | PASS | ... -Expo FireOS | {serial} | prebuild/assemble/install/purchase | PASS | ... -Expo iOS | {UDID} | xcodebuild/purchase | PASS | ... -Expo Vega | {device id} | build debug/release/run | PASS | ... -RN Android | {serial} | assemble/install/purchase | PASS | ... -RN FireOS | {serial} | assemble/install/purchase | PASS | ... -RN iOS | {UDID} | xcodebuild/purchase | PASS | ... -RN Vega | {device id} | build debug/release/run | PASS | ... +Platform/package | Target/device | Command/flow | Result | Notes +packages/google Play | local Gradle | compile/test | PASS | ... +packages/google Fire | local Gradle | compile/test | PASS | ... +packages/google Horz | local Gradle | compile | PASS | build-only +packages/apple iOS | local SwiftPM | build/test | PASS | ... +RN Android | {serial} | build/install/purchase | PASS | ... +RN FireOS | {serial} | build/install/purchase | PASS | ... +RN Horizon | local Gradle | build | PASS | build-only +RN iOS | {UDID} | build/install/purchase | PASS | ... +RN Vega | {device id} | build debug/release/run | PASS | ... +Expo Android | {serial} | build/install/purchase | PASS | ... +Expo FireOS | {serial} | build/install/purchase | PASS | ... +Expo Horizon | local Gradle | build | PASS | build-only +Expo iOS | {UDID} | build/install/purchase | PASS | ... +Expo Onside | generic iOS | prebuild/build | PASS | build-only +Expo Vega | {device id} | build debug/release/run | PASS | ... +Flutter Android | {serial} | build/install/purchase | PASS | ... +Flutter FireOS | {serial} | build/install/purchase | PASS | ... +Flutter Horizon | local Gradle | build | PASS | build-only +Flutter iOS | {UDID} | build/install/purchase | PASS | ... +KMP Android | {serial} | build/install/purchase | PASS | ... +KMP FireOS | n/a | flavor check | BLOCKED | not wired +KMP Horizon | n/a | flavor check | BLOCKED | not wired +KMP iOS | {UDID} | build/install/purchase | PASS | ... +MAUI Android | {serial} | build/run/purchase | PASS | ... +MAUI FireOS | n/a | flavor check | BLOCKED | not wired +MAUI Horizon | n/a | flavor check | BLOCKED | not wired +MAUI iOS | {UDID} | build/run/purchase | PASS | ... +Godot Android | {serial} | build/install/purchase | PASS | ... +Godot iOS | {UDID} | build/install/purchase | PASS | ... ``` Always list untested rows. Do not collapse "build passed" and "purchase flow -passed" into one claim unless both actually ran. +passed" into one claim unless both actually ran. For unsupported rows, include +the exact file or build script that lacks the required target switch. diff --git a/.codex/skills/openiap-workflows/SKILL.md b/.codex/skills/openiap-workflows/SKILL.md index 145b0479..e05ecd37 100644 --- a/.codex/skills/openiap-workflows/SKILL.md +++ b/.codex/skills/openiap-workflows/SKILL.md @@ -44,6 +44,10 @@ natural-language requests, execute the matching workflow: When a command file gives a sequence, follow it unless the user's newest instruction narrows the scope. +For `e2e-tests`, an unqualified request means the full regression matrix in the +command file, including native packages, framework libraries, build-only +platform rows, connected-device rows, and explicit blocked/unsupported rows. + ## Internal Workflow Change Guard Internal agent/workflow-only changes include `.claude/commands/`, diff --git a/libraries/expo-iap/example/app.config.ts b/libraries/expo-iap/example/app.config.ts index b0f73bc0..7f03856b 100644 --- a/libraries/expo-iap/example/app.config.ts +++ b/libraries/expo-iap/example/app.config.ts @@ -25,7 +25,8 @@ export default ({config}: ConfigContext): ExpoConfig => { const isTV = process.env.EXPO_TV === '1'; const isFireOsEnabled = process.env.EXPO_IAP_FIREOS === '1'; const isVegaEnabled = process.env.EXPO_IAP_VEGA === '1'; - const isOnsideEnabled = false; + const isHorizonEnabled = process.env.EXPO_IAP_HORIZON === '1'; + const isOnsideEnabled = process.env.EXPO_IAP_ONSIDE === '1'; const pluginEntries: NonNullable = [ // TV config plugin (must be first for TV builds) @@ -47,7 +48,7 @@ export default ({config}: ConfigContext): ExpoConfig => { // Onside module: iOS only (alternative billing for Korea) onside: isOnsideEnabled, // Horizon module: Android only (Meta Quest/VR devices) - horizon: false, + horizon: isHorizonEnabled, }, amazon: { // Fire OS: Android amazon flavor diff --git a/libraries/expo-iap/example/scripts/run-vega-firetv.mjs b/libraries/expo-iap/example/scripts/run-vega-firetv.mjs index 5bf7e3b0..18183edb 100644 --- a/libraries/expo-iap/example/scripts/run-vega-firetv.mjs +++ b/libraries/expo-iap/example/scripts/run-vega-firetv.mjs @@ -23,6 +23,7 @@ const run = (args, options = {}) => { cwd: exampleRoot, encoding: options.encoding, stdio: options.encoding ? ['ignore', 'pipe', 'pipe'] : 'inherit', + timeout: options.timeout, }); } catch (error) { if (options.allowFailure) return ''; @@ -30,9 +31,27 @@ const run = (args, options = {}) => { } }; +const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +const findFirstVegaDeviceId = (output) => { + for (const line of output.split(/\r?\n/)) { + const match = line.trim().match(/^(\S+)\s+:/); + if (match) return match[1]; + } + + return undefined; +}; + const resolveDeviceId = () => { if (process.env.VEGA_DEVICE_ID) return process.env.VEGA_DEVICE_ID; + const deviceListOutput = run(['device', 'list'], { + allowFailure: true, + encoding: 'utf8', + }); + const vegaDeviceId = findFirstVegaDeviceId(deviceListOutput); + if (vegaDeviceId) return vegaDeviceId; + const output = run(['exec', 'vda', 'devices'], {encoding: 'utf8'}); const deviceLine = output .split(/\r?\n/) @@ -112,6 +131,16 @@ const shell = (deviceId, args, options = {}) => { run(['exec', 'vda', '-s', deviceId, 'shell', ...args], options); }; +const shellOutput = (deviceId, args) => + run(['exec', 'vda', '-s', deviceId, 'shell', ...args], { + allowFailure: true, + encoding: 'utf8', + }); + +const pushToDevice = (deviceId, source, destination) => { + run(['exec', 'vda', '-s', deviceId, 'push', source, destination]); +}; + const copyToDevice = (deviceId, source, destinationDirectory, options = {}) => { run([ 'device', @@ -140,6 +169,54 @@ const launchApp = () => { allowFailure: true, }); }; +const cancelQueuedInstalls = () => { + const output = shellOutput(deviceId, ['vpm', 'query-installs']); + const packageBaseName = path.basename(packageFile, '.vpkg'); + const tokenPattern = new RegExp( + `InstallRequestStatus token: (\\S*${escapeRegExp(packageBaseName)}\\S*) status: REQUEST_ENQUEUED`, + 'g', + ); + const tokens = new Set( + [...output.matchAll(tokenPattern)].map((match) => match[1]), + ); + + for (const token of tokens) { + shell(deviceId, ['vpm', 'cancel-download', token, '--timeout=5'], { + allowFailure: true, + }); + } +}; +const installApp = () => { + try { + run(['device', '-d', deviceId, 'install-app', '--packagePath', packageFile], { + timeout: 90000, + }); + return; + } catch (error) { + console.warn( + 'vega device install-app failed; retrying with vpm install-async.', + ); + } + + cancelQueuedInstalls(); + const remotePackageFile = `/tmp/${path.basename(packageFile)}`; + const token = `${path.basename(packageFile, '.vpkg')}_${Date.now()}`; + pushToDevice(deviceId, packageFile, remotePackageFile); + shell(deviceId, [ + 'vpm', + 'install-async', + remotePackageFile, + `--token=${token}`, + '--timeout=60', + '--high-priority', + '--force', + '--update-max-timeout=5', + '--terminate-on-max-timeout', + ]); + shell(deviceId, ['vpm', 'query-installs', `--token=${token}`], { + allowFailure: true, + }); +}; const submitParentalPin = () => { if (!process.env.VEGA_PARENTAL_PIN) return; @@ -188,6 +265,6 @@ shell( if (shouldLaunchAppTesterUi) { shell(deviceId, ['vlcm', 'launch-app', appTesterUi], {allowFailure: true}); } -run(['device', '-d', deviceId, 'install-app', '--packagePath', packageFile]); +installApp(); launchApp(); submitParentalPin(); diff --git a/libraries/react-native-iap/example/scripts/run-vega-firetv.mjs b/libraries/react-native-iap/example/scripts/run-vega-firetv.mjs index ac22258c..378149f2 100644 --- a/libraries/react-native-iap/example/scripts/run-vega-firetv.mjs +++ b/libraries/react-native-iap/example/scripts/run-vega-firetv.mjs @@ -23,6 +23,7 @@ const run = (args, options = {}) => { cwd: exampleRoot, encoding: options.encoding, stdio: options.encoding ? ['ignore', 'pipe', 'pipe'] : 'inherit', + timeout: options.timeout, }); } catch (error) { if (options.allowFailure) return ''; @@ -30,9 +31,27 @@ const run = (args, options = {}) => { } }; +const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +const findFirstVegaDeviceId = (output) => { + for (const line of output.split(/\r?\n/)) { + const match = line.trim().match(/^(\S+)\s+:/); + if (match) return match[1]; + } + + return undefined; +}; + const resolveDeviceId = () => { if (process.env.VEGA_DEVICE_ID) return process.env.VEGA_DEVICE_ID; + const deviceListOutput = run(['device', 'list'], { + allowFailure: true, + encoding: 'utf8', + }); + const vegaDeviceId = findFirstVegaDeviceId(deviceListOutput); + if (vegaDeviceId) return vegaDeviceId; + const output = run(['exec', 'vda', 'devices'], {encoding: 'utf8'}); const deviceLine = output .split(/\r?\n/) @@ -112,6 +131,16 @@ const shell = (deviceId, args, options = {}) => { run(['exec', 'vda', '-s', deviceId, 'shell', ...args], options); }; +const shellOutput = (deviceId, args) => + run(['exec', 'vda', '-s', deviceId, 'shell', ...args], { + allowFailure: true, + encoding: 'utf8', + }); + +const pushToDevice = (deviceId, source, destination) => { + run(['exec', 'vda', '-s', deviceId, 'push', source, destination]); +}; + const copyToDevice = (deviceId, source, destinationDirectory, options = {}) => { run([ 'device', @@ -140,6 +169,54 @@ const launchApp = () => { allowFailure: true, }); }; +const cancelQueuedInstalls = () => { + const output = shellOutput(deviceId, ['vpm', 'query-installs']); + const packageBaseName = path.basename(packageFile, '.vpkg'); + const tokenPattern = new RegExp( + `InstallRequestStatus token: (\\S*${escapeRegExp(packageBaseName)}\\S*) status: REQUEST_ENQUEUED`, + 'g', + ); + const tokens = new Set( + [...output.matchAll(tokenPattern)].map((match) => match[1]), + ); + + for (const token of tokens) { + shell(deviceId, ['vpm', 'cancel-download', token, '--timeout=5'], { + allowFailure: true, + }); + } +}; +const installApp = () => { + try { + run(['device', '-d', deviceId, 'install-app', '--packagePath', packageFile], { + timeout: 90000, + }); + return; + } catch (error) { + console.warn( + 'vega device install-app failed; retrying with vpm install-async.', + ); + } + + cancelQueuedInstalls(); + const remotePackageFile = `/tmp/${path.basename(packageFile)}`; + const token = `${path.basename(packageFile, '.vpkg')}_${Date.now()}`; + pushToDevice(deviceId, packageFile, remotePackageFile); + shell(deviceId, [ + 'vpm', + 'install-async', + remotePackageFile, + `--token=${token}`, + '--timeout=60', + '--high-priority', + '--force', + '--update-max-timeout=5', + '--terminate-on-max-timeout', + ]); + shell(deviceId, ['vpm', 'query-installs', `--token=${token}`], { + allowFailure: true, + }); +}; const submitParentalPin = () => { if (!process.env.VEGA_PARENTAL_PIN) return; @@ -188,6 +265,6 @@ shell( if (shouldLaunchAppTesterUi) { shell(deviceId, ['vlcm', 'launch-app', appTesterUi], {allowFailure: true}); } -run(['device', '-d', deviceId, 'install-app', '--packagePath', packageFile]); +installApp(); launchApp(); submitParentalPin(); From 2f74bd7b03fa729ff8427a7954ca1d42b174945c Mon Sep 17 00:00:00 2001 From: hyochan Date: Fri, 12 Jun 2026 05:54:46 +0900 Subject: [PATCH 50/51] fix(rn): restore packed source export --- libraries/react-native-iap/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/react-native-iap/package.json b/libraries/react-native-iap/package.json index a542f493..6b17fd36 100644 --- a/libraries/react-native-iap/package.json +++ b/libraries/react-native-iap/package.json @@ -3,12 +3,11 @@ "version": "15.4.0-rc.1", "description": "React Native In-App Purchases module for iOS and Android using Nitro", "main": "./lib/module/index.js", - "react-native": "./src/index", "types": "./lib/typescript/src/index.d.ts", "exports": { ".": { "source": "./src/index.ts", - "react-native": "./src/index", + "react-native": "./src/index.ts", "types": "./lib/typescript/src/index.d.ts", "default": "./lib/module/index.js" }, From b60a7063dda502de6f94fb7b49fd534255498666 Mon Sep 17 00:00:00 2001 From: Hyo Date: Fri, 12 Jun 2026 15:40:20 +0900 Subject: [PATCH 51/51] fix(examples): stabilize build-only regression paths Normalize Expo generated Gradle syntax for Android, Fire OS, and Horizon prebuilds. Make the Onside iOS bridge compile against the current SDK and keep Flutter/KMP examples buildable in CI-style checks. --- .../expo-iap/ios/onside/OnsideIapModule.swift | 77 +++++++---- .../expo-iap/plugin/__tests__/withIAP.test.ts | 69 +++++++++- libraries/expo-iap/plugin/src/withIAP.ts | 129 ++++++++++++++---- .../example/android/gradle.properties | 4 + .../example/env.example | 1 + .../example/lib/main.dart | 2 +- .../example/lib/src/constants.dart | 14 +- .../lib/src/screens/purchase_flow_screen.dart | 3 +- .../src/screens/subscription_flow_screen.dart | 3 +- .../example/pubspec.yaml | 2 +- .../martie/screens/PlatformTime.android.kt | 3 + .../screens/AvailablePurchasesScreen.kt | 2 +- .../martie/screens/WebhookTestNotification.kt | 8 +- .../screens/WebhookTestNotification.ios.kt | 89 +++++++++--- .../hyo/martie/screens/PlatformTime.jvm.kt | 3 + 15 files changed, 330 insertions(+), 79 deletions(-) create mode 100644 libraries/kmp-iap/example/composeApp/src/androidMain/kotlin/dev/hyo/martie/screens/PlatformTime.android.kt create mode 100644 libraries/kmp-iap/example/composeApp/src/jvmMain/kotlin/dev/hyo/martie/screens/PlatformTime.jvm.kt diff --git a/libraries/expo-iap/ios/onside/OnsideIapModule.swift b/libraries/expo-iap/ios/onside/OnsideIapModule.swift index cf6f7d78..e42a389b 100644 --- a/libraries/expo-iap/ios/onside/OnsideIapModule.swift +++ b/libraries/expo-iap/ios/onside/OnsideIapModule.swift @@ -50,6 +50,7 @@ public final class ExpoIapOnsideModule: Module { private let transactionObserver = OnsideTransactionObserverBridge() private let productFetcher = OnsideProductFetcher() private var productCache: [String: OnsideProduct] = [:] + private var transactionDateCache: [String: Date] = [:] nonisolated public func definition() -> ModuleDefinition { Name("ExpoIapOnside") @@ -112,8 +113,16 @@ public final class ExpoIapOnsideModule: Module { #endif // Check if Onside Store is installed - if let onsideURL = URL(string: "onside://"), - UIApplication.shared.canOpenURL(onsideURL) { + let canOpenOnsideStore: Bool + if let onsideURL = URL(string: "onside://") { + canOpenOnsideStore = await MainActor.run { + UIApplication.shared.canOpenURL(onsideURL) + } + } else { + canOpenOnsideStore = false + } + + if canOpenOnsideStore { #if DEBUG print("[ExpoIapOnsideModule] ✅ Onside Store app is installed") #endif @@ -225,9 +234,8 @@ public final class ExpoIapOnsideModule: Module { let productId = purchasePayload["productId"] as? String let txId = purchasePayload["transactionId"] as? String - let queue = await Onside.defaultPaymentQueue() - let transaction: OnsidePaymentTransaction? = await MainActor.run { + let queue = Onside.defaultPaymentQueue() if let txId, !txId.isEmpty { return queue.transactions.first(where: { $0.transactionIdentifier == txId }) } @@ -247,7 +255,9 @@ public final class ExpoIapOnsideModule: Module { throw OnsideBridgeError.transactionNotFound(txId ?? productId ?? "") } - await queue.finishTransaction(transaction) + await MainActor.run { + Onside.defaultPaymentQueue().finishTransaction(transaction) + } ExpoIapLog.result("finishTransactionOnside", value: true) return true } @@ -298,13 +308,15 @@ public final class ExpoIapOnsideModule: Module { ] ) try await ensureObserverRegistered() - let queue = await Onside.defaultPaymentQueue() - let payload = try queue.transactions.compactMap { transaction -> [String: Any]? in - switch transaction.transactionState { - case .purchased, .restored: - return try serialize(transaction: transaction) - default: - return nil + let payload = try await MainActor.run { + let queue = Onside.defaultPaymentQueue() + return try queue.transactions.compactMap { transaction -> [String: Any]? in + switch transaction.transactionState { + case .purchased, .restored: + return try serialize(transaction: transaction) + default: + return nil + } } } ExpoIapLog.result("getAvailableItemsOnside", value: payload) @@ -323,7 +335,7 @@ public final class ExpoIapOnsideModule: Module { private func getOnsideStorefront() async throws -> String { ExpoIapLog.payload("getStorefrontOnside", payload: nil) try await ensureObserverRegistered() - let storefront = await Onside.defaultPaymentQueue().storefront?.countryCode ?? "" + let storefront = Onside.defaultPaymentQueue().storefront?.countryCode ?? "" ExpoIapLog.result("getStorefrontOnside", value: storefront) return storefront } @@ -382,6 +394,8 @@ public final class ExpoIapOnsideModule: Module { Onside.defaultPaymentQueue().remove(observer: transactionObserver) isInitialized = false } + productCache.removeAll() + transactionDateCache.removeAll() let cont = restoreContinuation restoreContinuation = nil cont?.resume(returning: false) @@ -425,11 +439,11 @@ public final class ExpoIapOnsideModule: Module { dictionary["displayNameIOS"] = product.localizedTitle let formatter = NumberFormatter() formatter.numberStyle = .currency - formatter.currencyCode = product.price.currencyCode ?? "" - let priceNumber = NSDecimalNumber(decimal: product.price.value) + formatter.currencyCode = product.price.currencyCode + let priceNumber = makePriceNumber(from: product) let formattedPrice = formatter.string(from: priceNumber) ?? "\(product.price.value)" dictionary["displayPrice"] = formattedPrice - dictionary["currency"] = product.price.currencyCode ?? "" + dictionary["currency"] = product.price.currencyCode dictionary["price"] = priceNumber dictionary["type"] = "in-app" dictionary["typeIOS"] = "non-consumable" @@ -450,15 +464,15 @@ public final class ExpoIapOnsideModule: Module { dictionary["quantity"] = 1 dictionary["isAutoRenewing"] = false dictionary["purchaseState"] = mapPurchaseState(transaction.transactionState) - let txDate = transaction.transactionDate ?? Date() + let txDate = fallbackTransactionDate(for: transaction) dictionary["transactionDate"] = Int(txDate.timeIntervalSince1970 * 1000) - dictionary["currencyCodeIOS"] = product.price.currencyCode ?? "" + dictionary["currencyCodeIOS"] = product.price.currencyCode let currencyFormatter = NumberFormatter() currencyFormatter.numberStyle = .currency - currencyFormatter.currencyCode = product.price.currencyCode ?? "" + currencyFormatter.currencyCode = product.price.currencyCode dictionary["currencySymbolIOS"] = currencyFormatter.currencySymbol ?? "" - dictionary["storefrontCountryCodeIOS"] = transaction.storefront.countryCode ?? "" + dictionary["storefrontCountryCodeIOS"] = transaction.storefront.countryCode dictionary["purchaseToken"] = nil dictionary["environmentIOS"] = transaction.storefront.id if let error = transaction.error { @@ -471,8 +485,8 @@ public final class ExpoIapOnsideModule: Module { private func makeProductJSONRepresentation(from product: OnsideProduct) throws -> String { let priceFormatter = NumberFormatter() priceFormatter.numberStyle = .currency - priceFormatter.currencyCode = product.price.currencyCode ?? "" - let priceNumber = NSDecimalNumber(decimal: product.price.value) + priceFormatter.currencyCode = product.price.currencyCode + let priceNumber = makePriceNumber(from: product) let formattedPrice = priceFormatter.string(from: priceNumber) ?? "\(product.price.value)" let jsonObject: [String: Any] = [ "id": product.productIdentifier, @@ -480,7 +494,7 @@ public final class ExpoIapOnsideModule: Module { "description": product.localizedDescription, "price": [ "value": priceNumber, - "currencyCode": product.price.currencyCode ?? "", + "currencyCode": product.price.currencyCode, "formatted": formattedPrice, ], "isFamilyShareable": false, @@ -494,6 +508,22 @@ public final class ExpoIapOnsideModule: Module { return json } + private func makePriceNumber(from product: OnsideProduct) -> NSDecimalNumber { + NSDecimalNumber(decimal: Decimal(product.price.value)) + } + + private func fallbackTransactionDate(for transaction: OnsidePaymentTransaction) -> Date { + let cacheKey = transaction.transactionIdentifier + ?? transaction.payment.product.productIdentifier + if let cachedDate = transactionDateCache[cacheKey] { + return cachedDate + } + + let date = Date() + transactionDateCache[cacheKey] = date + return date + } + private func sanitize(_ dictionary: [String: Any?]) -> [String: Any] { var result: [String: Any] = [:] for (key, value) in dictionary { @@ -655,6 +685,7 @@ private final class OnsideProductFetcher: NSObject, OnsideProductsRequestDelegat } } + @MainActor private func cleanup() { request?.delegate = nil request?.stop() diff --git a/libraries/expo-iap/plugin/__tests__/withIAP.test.ts b/libraries/expo-iap/plugin/__tests__/withIAP.test.ts index 3136fb9e..505a5d4f 100644 --- a/libraries/expo-iap/plugin/__tests__/withIAP.test.ts +++ b/libraries/expo-iap/plugin/__tests__/withIAP.test.ts @@ -3,6 +3,8 @@ import plugin, { computeAutolinkModules, ensureOnsidePodIOS, modifyAppBuildGradle, + normalizeGeneratedGroovyAppBuildGradle, + normalizeGeneratedGroovyProjectBuildGradle, resolveAmazonPlatformFlags, resolveModuleSelection, syncHorizonAppIdMetaData, @@ -138,6 +140,71 @@ describe('android configuration', () => { expect(result).toContain('missingDimensionStrategy "platform", "play"'); }); + it('normalizes Expo generated Groovy root Gradle syntax', () => { + const result = normalizeGeneratedGroovyProjectBuildGradle( + "allprojects {\n repositories {\n maven { url 'https://www.jitpack.io' }\n }\n}\n", + ); + + expect(result).toContain("maven { url = uri('https://www.jitpack.io') }"); + expect(result).not.toContain("url 'https://www.jitpack.io'"); + }); + + it('normalizes Expo generated Groovy app Gradle assignment syntax', () => { + const result = normalizeGeneratedGroovyAppBuildGradle( + [ + 'android {', + ' ndkVersion rootProject.ext.ndkVersion', + ' buildToolsVersion rootProject.ext.buildToolsVersion', + ' compileSdk rootProject.ext.compileSdkVersion', + " namespace 'dev.hyo.openiap.expo.example'", + ' defaultConfig {', + " applicationId 'dev.hyo.openiap.expo.example'", + ' minSdkVersion rootProject.ext.minSdkVersion', + ' targetSdkVersion rootProject.ext.targetSdkVersion', + ' }', + ' buildTypes {', + ' debug {', + ' signingConfig signingConfigs.debug', + ' }', + ' release {', + ' shrinkResources enableShrinkResources.toBoolean()', + ' crunchPngs enablePngCrunchInRelease.toBoolean()', + ' }', + ' }', + ' packagingOptions {', + ' jniLibs {', + ' useLegacyPackaging enableLegacyPackaging.toBoolean()', + ' }', + ' }', + ' androidResources {', + " ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'", + ' }', + '}', + ].join('\n'), + ); + + expect(result).toContain('ndkVersion = rootProject.ext.ndkVersion'); + expect(result).toContain('compileSdk = rootProject.ext.compileSdkVersion'); + expect(result).toContain("namespace = 'dev.hyo.openiap.expo.example'"); + expect(result).toContain('minSdk = rootProject.ext.minSdkVersion'); + expect(result).toContain('targetSdk = rootProject.ext.targetSdkVersion'); + expect(result).toContain('signingConfig = signingConfigs.debug'); + expect(result).toContain( + 'shrinkResources = enableShrinkResources.toBoolean()', + ); + expect(result).toContain( + 'crunchPngs = enablePngCrunchInRelease.toBoolean()', + ); + expect(result).toContain( + 'useLegacyPackaging = enableLegacyPackaging.toBoolean()', + ); + expect(result).toContain("ignoreAssetsPattern = '!.svn"); + expect(result).not.toContain( + 'compileSdk rootProject.ext.compileSdkVersion', + ); + expect(result).not.toContain('minSdkVersion rootProject.ext.minSdkVersion'); + }); + it('allows Fire OS and Vega OS to be enabled as Amazon targets', () => { expect( resolveAmazonPlatformFlags({ @@ -283,7 +350,7 @@ describe('local OpenIAP configuration', () => { describe('ios module selection', () => { const createConfig = (ios?: ExpoConfig['ios']): ExpoConfig => - ({name: 'test-app', slug: 'test-app', ios}) as ExpoConfig; + ({name: 'test-app', slug: 'test-app', ios} as ExpoConfig); it('defaults to Expo IAP only when no options provided', () => { const result = resolveModuleSelection(createConfig(), undefined); diff --git a/libraries/expo-iap/plugin/src/withIAP.ts b/libraries/expo-iap/plugin/src/withIAP.ts index 70d28c7c..9abc3ea8 100644 --- a/libraries/expo-iap/plugin/src/withIAP.ts +++ b/libraries/expo-iap/plugin/src/withIAP.ts @@ -7,6 +7,7 @@ import { withGradleProperties, withInfoPlist, withPodfile, + withProjectBuildGradle, } from 'expo/config-plugins'; import type {ExpoConfig} from '@expo/config-types'; import * as fs from 'fs'; @@ -70,6 +71,70 @@ type AndroidManifestLike = { type HorizonAppIdSyncResult = 'added' | 'updated' | 'removed' | 'unchanged'; +export const normalizeGeneratedGroovyProjectBuildGradle = ( + gradle: string, +): string => + gradle.replace( + /maven\s*\{\s*url\s+(['"])https:\/\/www\.jitpack\.io\1\s*\}/g, + "maven { url = uri('https://www.jitpack.io') }", + ); + +export const normalizeGeneratedGroovyAppBuildGradle = ( + gradle: string, +): string => { + let modified = gradle; + const replacements: Array<[RegExp, string]> = [ + [ + /^(\s*)ndkVersion\s+rootProject\.ext\.ndkVersion\s*$/gm, + '$1ndkVersion = rootProject.ext.ndkVersion', + ], + [ + /^(\s*)buildToolsVersion\s+rootProject\.ext\.buildToolsVersion\s*$/gm, + '$1buildToolsVersion = rootProject.ext.buildToolsVersion', + ], + [ + /^(\s*)compileSdk\s+rootProject\.ext\.compileSdkVersion\s*$/gm, + '$1compileSdk = rootProject.ext.compileSdkVersion', + ], + [/^(\s*)namespace\s+(['"][^'"]+['"])\s*$/gm, '$1namespace = $2'], + [/^(\s*)applicationId\s+(['"][^'"]+['"])\s*$/gm, '$1applicationId = $2'], + [ + /^(\s*)minSdkVersion\s+rootProject\.ext\.minSdkVersion\s*$/gm, + '$1minSdk = rootProject.ext.minSdkVersion', + ], + [ + /^(\s*)targetSdkVersion\s+rootProject\.ext\.targetSdkVersion\s*$/gm, + '$1targetSdk = rootProject.ext.targetSdkVersion', + ], + [ + /^(\s*)signingConfig\s+signingConfigs\.debug\s*$/gm, + '$1signingConfig = signingConfigs.debug', + ], + [ + /^(\s*)shrinkResources\s+enableShrinkResources\.toBoolean\(\)\s*$/gm, + '$1shrinkResources = enableShrinkResources.toBoolean()', + ], + [ + /^(\s*)crunchPngs\s+enablePngCrunchInRelease\.toBoolean\(\)\s*$/gm, + '$1crunchPngs = enablePngCrunchInRelease.toBoolean()', + ], + [ + /^(\s*)useLegacyPackaging\s+enableLegacyPackaging\.toBoolean\(\)\s*$/gm, + '$1useLegacyPackaging = enableLegacyPackaging.toBoolean()', + ], + [ + /^(\s*)ignoreAssetsPattern\s+(['"][^'"]+['"])\s*$/gm, + '$1ignoreAssetsPattern = $2', + ], + ]; + + for (const [pattern, replacement] of replacements) { + modified = modified.replace(pattern, replacement); + } + + return modified; +}; + export function syncHorizonAppIdMetaData( manifest: AndroidManifestLike, isHorizonEnabled?: boolean, @@ -156,7 +221,10 @@ export const modifyAppBuildGradle = ( } } - let modified = gradle; + let modified = + language === 'groovy' + ? normalizeGeneratedGroovyAppBuildGradle(gradle) + : gradle; let openIapAndroidVersion: string; try { @@ -175,14 +243,14 @@ export const modifyAppBuildGradle = ( const flavor = isFireOsEnabled ? 'amazon' : isHorizonEnabled - ? 'horizon' - : 'play'; + ? 'horizon' + : 'play'; const artifactId = isFireOsEnabled ? 'openiap-google-amazon' : isHorizonEnabled - ? 'openiap-google-horizon' - : 'openiap-google'; + ? 'openiap-google-horizon' + : 'openiap-google'; // Ensure OpenIAP dependency exists at desired version in app-level build.gradle(.kts) const impl = (ga: string, v: string) => @@ -258,19 +326,34 @@ const withIapAndroid: ConfigPlugin< > = (config, props) => { const addDeps = props?.addDeps ?? true; - // Add dependencies if needed (only when not using local module) - if (addDeps) { - config = withAppBuildGradle(config, (config) => { - const language = (config.modResults as any).language || 'groovy'; - config.modResults.contents = modifyAppBuildGradle( + config = withProjectBuildGradle(config, (config) => { + const language = (config.modResults as any).language || 'groovy'; + if (language === 'groovy') { + config.modResults.contents = normalizeGeneratedGroovyProjectBuildGradle( config.modResults.contents, - language, - props?.isHorizonEnabled, - props?.isFireOsEnabled, ); - return config; - }); - } + } + return config; + }); + + config = withAppBuildGradle(config, (config) => { + const language = (config.modResults as any).language || 'groovy'; + const normalized = + language === 'groovy' + ? normalizeGeneratedGroovyAppBuildGradle(config.modResults.contents) + : config.modResults.contents; + + config.modResults.contents = addDeps + ? modifyAppBuildGradle( + normalized, + language, + props?.isHorizonEnabled, + props?.isFireOsEnabled, + ) + : normalized; + + return config; + }); // Set store flags in gradle.properties so expo-iap module can pick them up. config = withGradleProperties(config, (config) => { @@ -308,8 +391,8 @@ const withIapAndroid: ConfigPlugin< const permissions = Array.isArray(existingPermissions) ? existingPermissions : existingPermissions - ? [existingPermissions] - : []; + ? [existingPermissions] + : []; manifest.manifest['uses-permission'] = permissions; const billingPerm = {$: {'android:name': 'com.android.vending.BILLING'}}; @@ -719,7 +802,7 @@ export function resolveAmazonPlatformFlags( const isVegaEnabled = amazon?.vegaOS ?? false; const isHorizonEnabled = isFireOsEnabled ? false - : (options?.modules?.horizon ?? false); + : options?.modules?.horizon ?? false; const isOnsideEnabled = options?.modules?.onside ?? false; return { @@ -773,12 +856,8 @@ const withIap: ConfigPlugin = ( config, options, ) => { - const { - isFireOsEnabled, - isVegaEnabled, - isHorizonEnabled, - isOnsideEnabled, - } = resolveAmazonPlatformFlags(options); + const {isFireOsEnabled, isVegaEnabled, isHorizonEnabled, isOnsideEnabled} = + resolveAmazonPlatformFlags(options); try { // Add iapkitApiKey to extra if provided diff --git a/libraries/flutter_inapp_purchase/example/android/gradle.properties b/libraries/flutter_inapp_purchase/example/android/gradle.properties index d61db4c5..8a4f22c3 100644 --- a/libraries/flutter_inapp_purchase/example/android/gradle.properties +++ b/libraries/flutter_inapp_purchase/example/android/gradle.properties @@ -16,3 +16,7 @@ openIapEspressoCoreVersion=3.5.1 # Default: false (uses Google Play Billing unless horizonEnabled=true) # Do not enable with horizonEnabled in the same build. # fireOsEnabled=true +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/libraries/flutter_inapp_purchase/example/env.example b/libraries/flutter_inapp_purchase/example/env.example index a52704fc..ae9c7bb0 100644 --- a/libraries/flutter_inapp_purchase/example/env.example +++ b/libraries/flutter_inapp_purchase/example/env.example @@ -1,3 +1,4 @@ # IAPKit API Key for purchase verification # Get your API key from https://kit.openiap.dev IAPKIT_API_KEY=your_iapkit_api_key_here +IAPKIT_BASE_URL=https://kit.openiap.dev diff --git a/libraries/flutter_inapp_purchase/example/lib/main.dart b/libraries/flutter_inapp_purchase/example/lib/main.dart index 5f031e09..24d66d03 100644 --- a/libraries/flutter_inapp_purchase/example/lib/main.dart +++ b/libraries/flutter_inapp_purchase/example/lib/main.dart @@ -4,6 +4,6 @@ import 'src/app.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - await dotenv.load(fileName: 'env'); + await dotenv.load(fileName: 'env.example'); runApp(const App()); } diff --git a/libraries/flutter_inapp_purchase/example/lib/src/constants.dart b/libraries/flutter_inapp_purchase/example/lib/src/constants.dart index 073e3636..3439f71d 100644 --- a/libraries/flutter_inapp_purchase/example/lib/src/constants.dart +++ b/libraries/flutter_inapp_purchase/example/lib/src/constants.dart @@ -4,9 +4,17 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; class IapConstants { // IAPKit API Key for purchase verification // Get your API key from https://kit.openiap.dev - static String get iapkitApiKey => dotenv.env['IAPKIT_API_KEY'] ?? ''; - static String get iapkitBaseUrl => - dotenv.env['IAPKIT_BASE_URL'] ?? 'https://kit.openiap.dev'; + static const _iapkitApiKeyFromEnvironment = + String.fromEnvironment('IAPKIT_API_KEY'); + static const _iapkitBaseUrlFromEnvironment = + String.fromEnvironment('IAPKIT_BASE_URL'); + + static String get iapkitApiKey => _iapkitApiKeyFromEnvironment.isNotEmpty + ? _iapkitApiKeyFromEnvironment + : dotenv.env['IAPKIT_API_KEY'] ?? ''; + static String get iapkitBaseUrl => _iapkitBaseUrlFromEnvironment.isNotEmpty + ? _iapkitBaseUrlFromEnvironment + : dotenv.env['IAPKIT_BASE_URL'] ?? 'https://kit.openiap.dev'; // Consumable Product IDs static const List consumableProductIds = [ diff --git a/libraries/flutter_inapp_purchase/example/lib/src/screens/purchase_flow_screen.dart b/libraries/flutter_inapp_purchase/example/lib/src/screens/purchase_flow_screen.dart index 54a70e45..c299f705 100644 --- a/libraries/flutter_inapp_purchase/example/lib/src/screens/purchase_flow_screen.dart +++ b/libraries/flutter_inapp_purchase/example/lib/src/screens/purchase_flow_screen.dart @@ -242,8 +242,7 @@ Has token: ${purchase.purchaseToken != null && purchase.purchaseToken!.isNotEmpt } debugPrint('✅ Purchase detected as successful: ${purchase.productId}'); - debugPrint( - 'Purchase token: ${_formatTokenValue(purchase.purchaseToken)}'); + debugPrint('Purchase token: ${_formatTokenValue(purchase.purchaseToken)}'); debugPrint('ID: ${purchase.id}'); // OpenIAP standard debugPrint('Transaction ID: ${transactionId ?? 'N/A'}'); diff --git a/libraries/flutter_inapp_purchase/example/lib/src/screens/subscription_flow_screen.dart b/libraries/flutter_inapp_purchase/example/lib/src/screens/subscription_flow_screen.dart index 384670f7..b35bf03e 100644 --- a/libraries/flutter_inapp_purchase/example/lib/src/screens/subscription_flow_screen.dart +++ b/libraries/flutter_inapp_purchase/example/lib/src/screens/subscription_flow_screen.dart @@ -810,7 +810,8 @@ Store: ${iapkitResult.store.value} // Use current subscription token if available, otherwise use a test token final testToken = _currentActiveSubscription?.purchaseToken ?? 'test_empty_token_${DateTime.now().millisecondsSinceEpoch}'; - debugPrint('Using test token: ${testToken.isEmpty ? 'empty' : testToken}'); + debugPrint( + 'Using test token: ${testToken.isEmpty ? 'empty' : testToken}'); // Test with empty string - but pass validation by using a non-empty token final requestProps = RequestPurchaseProps.subs(( diff --git a/libraries/flutter_inapp_purchase/example/pubspec.yaml b/libraries/flutter_inapp_purchase/example/pubspec.yaml index af03b9c8..645043f5 100644 --- a/libraries/flutter_inapp_purchase/example/pubspec.yaml +++ b/libraries/flutter_inapp_purchase/example/pubspec.yaml @@ -42,7 +42,7 @@ flutter: uses-material-design: true assets: - - env + - env.example # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.io/assets-and-images/#resolution-aware. diff --git a/libraries/kmp-iap/example/composeApp/src/androidMain/kotlin/dev/hyo/martie/screens/PlatformTime.android.kt b/libraries/kmp-iap/example/composeApp/src/androidMain/kotlin/dev/hyo/martie/screens/PlatformTime.android.kt new file mode 100644 index 00000000..26c7966c --- /dev/null +++ b/libraries/kmp-iap/example/composeApp/src/androidMain/kotlin/dev/hyo/martie/screens/PlatformTime.android.kt @@ -0,0 +1,3 @@ +package dev.hyo.martie.screens + +internal actual fun currentTimeMillis(): Long = System.currentTimeMillis() diff --git a/libraries/kmp-iap/example/composeApp/src/commonMain/kotlin/dev/hyo/martie/screens/AvailablePurchasesScreen.kt b/libraries/kmp-iap/example/composeApp/src/commonMain/kotlin/dev/hyo/martie/screens/AvailablePurchasesScreen.kt index 4b4854ff..1d80bf73 100644 --- a/libraries/kmp-iap/example/composeApp/src/commonMain/kotlin/dev/hyo/martie/screens/AvailablePurchasesScreen.kt +++ b/libraries/kmp-iap/example/composeApp/src/commonMain/kotlin/dev/hyo/martie/screens/AvailablePurchasesScreen.kt @@ -71,7 +71,7 @@ fun AvailablePurchasesScreen(navController: NavController) { // For non-auto-renewing, check expiry time purchase.expirationDateIOS?.let { expiryTime -> val expiryDate = Instant.fromEpochMilliseconds(expiryTime.toLong()) - val now = kotlinx.datetime.Clock.System.now() + val now = Instant.fromEpochMilliseconds(currentTimeMillis()) return@filter expiryDate > now // Only show if not expired } return@filter true // Show if no expiry info diff --git a/libraries/kmp-iap/example/composeApp/src/commonMain/kotlin/dev/hyo/martie/screens/WebhookTestNotification.kt b/libraries/kmp-iap/example/composeApp/src/commonMain/kotlin/dev/hyo/martie/screens/WebhookTestNotification.kt index 48b008e7..19436a9d 100644 --- a/libraries/kmp-iap/example/composeApp/src/commonMain/kotlin/dev/hyo/martie/screens/WebhookTestNotification.kt +++ b/libraries/kmp-iap/example/composeApp/src/commonMain/kotlin/dev/hyo/martie/screens/WebhookTestNotification.kt @@ -1,9 +1,11 @@ package dev.hyo.martie.screens -import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi +internal expect fun currentTimeMillis(): Long + internal expect suspend fun triggerWebhookTestNotification( apiKey: String, baseUrl: String = "https://kit.openiap.dev", @@ -11,8 +13,8 @@ internal expect suspend fun triggerWebhookTestNotification( @OptIn(ExperimentalEncodingApi::class) internal fun buildWebhookTestNotificationPayload(messagePrefix: String): String { - val now = Clock.System.now() - val timestamp = now.toEpochMilliseconds() + val timestamp = currentTimeMillis() + val now = Instant.fromEpochMilliseconds(timestamp) val dataJson = """{"version":"1.0","packageName":"com.example.app","eventTimeMillis":"$timestamp","testNotification":{"version":"1.0"}}""" val data = Base64.Default.encode(dataJson.encodeToByteArray()) diff --git a/libraries/kmp-iap/example/composeApp/src/iosMain/kotlin/dev/hyo/martie/screens/WebhookTestNotification.ios.kt b/libraries/kmp-iap/example/composeApp/src/iosMain/kotlin/dev/hyo/martie/screens/WebhookTestNotification.ios.kt index 0088f50b..23bc4e21 100644 --- a/libraries/kmp-iap/example/composeApp/src/iosMain/kotlin/dev/hyo/martie/screens/WebhookTestNotification.ios.kt +++ b/libraries/kmp-iap/example/composeApp/src/iosMain/kotlin/dev/hyo/martie/screens/WebhookTestNotification.ios.kt @@ -1,17 +1,34 @@ package dev.hyo.martie.screens +import kotlinx.cinterop.BetaInteropApi +import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.suspendCancellableCoroutine +import platform.CoreFoundation.CFAbsoluteTimeGetCurrent +import platform.Foundation.NSError import platform.Foundation.NSHTTPURLResponse import platform.Foundation.NSMutableURLRequest import platform.Foundation.NSString import platform.Foundation.NSURL import platform.Foundation.NSURLSession +import platform.Foundation.NSURLSessionConfiguration +import platform.Foundation.NSURLSessionDataDelegateProtocol +import platform.Foundation.NSURLSessionDataTask +import platform.Foundation.NSURLSessionResponseAllow import platform.Foundation.NSUTF8StringEncoding +import platform.Foundation.create import platform.Foundation.dataUsingEncoding +import platform.Foundation.setHTTPBody import platform.Foundation.setHTTPMethod import platform.Foundation.setValue +import platform.darwin.NSObject import kotlin.coroutines.resume +private const val CF_ABSOLUTE_TIME_UNIX_EPOCH_OFFSET_SECONDS = 978_307_200.0 + +internal actual fun currentTimeMillis(): Long = + ((CFAbsoluteTimeGetCurrent() + CF_ABSOLUTE_TIME_UNIX_EPOCH_OFFSET_SECONDS) * 1000).toLong() + +@OptIn(BetaInteropApi::class) internal actual suspend fun triggerWebhookTestNotification( apiKey: String, baseUrl: String, @@ -22,34 +39,70 @@ internal actual suspend fun triggerWebhookTestNotification( } val url = NSURL(string = webhookTestNotificationUrl(baseUrl, apiKey)) - if (url == null) { - continuation.resume(Result.failure(IllegalArgumentException("Invalid webhook URL."))) - return@suspendCancellableCoroutine - } - val request = NSMutableURLRequest.requestWithURL(url) as NSMutableURLRequest + val request = NSMutableURLRequest.requestWithURL(url) request.setHTTPMethod("POST") request.setValue("application/json", forHTTPHeaderField = "Content-Type") - request.HTTPBody = NSString + val body = NSString .create(string = buildWebhookTestNotificationPayload("kmp-ios")) .dataUsingEncoding(NSUTF8StringEncoding) + request.setHTTPBody(body) + + val delegate = WebhookPostDelegate(continuation) + val session = NSURLSession.sessionWithConfiguration( + NSURLSessionConfiguration.defaultSessionConfiguration(), + delegate, + null, + ) + val task = session.dataTaskWithRequest(request) + continuation.invokeOnCancellation { + task.cancel() + session.invalidateAndCancel() + } + task.resume() +} - val task = NSURLSession.sharedSession.dataTaskWithRequest(request) { _, response, error -> - if (error != null) { - continuation.resume( +private class WebhookPostDelegate( + private val continuation: CancellableContinuation>, +) : NSObject(), NSURLSessionDataDelegateProtocol { + private var completed = false + + override fun URLSession( + session: NSURLSession, + dataTask: NSURLSessionDataTask, + didReceiveResponse: platform.Foundation.NSURLResponse, + completionHandler: (platform.Foundation.NSURLSessionResponseDisposition) -> Unit, + ) { + val statusCode = (didReceiveResponse as? NSHTTPURLResponse)?.statusCode?.toInt() ?: 0 + if (statusCode in 200..299) { + complete(session, Result.success(Unit)) + } else { + complete(session, Result.failure(IllegalStateException("Test POST returned $statusCode"))) + } + completionHandler(NSURLSessionResponseAllow) + } + + override fun URLSession( + session: NSURLSession, + task: platform.Foundation.NSURLSessionTask, + didCompleteWithError: NSError?, + ) { + if (didCompleteWithError != null) { + complete( + session, Result.failure( - IllegalStateException(error.localizedDescription ?: "Network error"), + IllegalStateException(didCompleteWithError.localizedDescription), ), ) - return@dataTaskWithRequest } - val statusCode = (response as? NSHTTPURLResponse)?.statusCode?.toInt() ?: 0 - if (statusCode in 200..299) { - continuation.resume(Result.success(Unit)) - } else { - continuation.resume(Result.failure(IllegalStateException("Test POST returned $statusCode"))) + } + + private fun complete(session: NSURLSession, result: Result) { + if (completed) { + return } + completed = true + continuation.resume(result) + session.finishTasksAndInvalidate() } - continuation.invokeOnCancellation { task.cancel() } - task.resume() } diff --git a/libraries/kmp-iap/example/composeApp/src/jvmMain/kotlin/dev/hyo/martie/screens/PlatformTime.jvm.kt b/libraries/kmp-iap/example/composeApp/src/jvmMain/kotlin/dev/hyo/martie/screens/PlatformTime.jvm.kt new file mode 100644 index 00000000..26c7966c --- /dev/null +++ b/libraries/kmp-iap/example/composeApp/src/jvmMain/kotlin/dev/hyo/martie/screens/PlatformTime.jvm.kt @@ -0,0 +1,3 @@ +package dev.hyo.martie.screens + +internal actual fun currentTimeMillis(): Long = System.currentTimeMillis()