diff --git a/.github/workflows/ci-expo-iap.yml b/.github/workflows/ci-expo-iap.yml index aa30aa22..91918338 100644 --- a/.github/workflows/ci-expo-iap.yml +++ b/.github/workflows/ci-expo-iap.yml @@ -32,7 +32,7 @@ jobs: - uses: oven-sh/setup-bun@v2 with: - bun-version: "1.1.38" + bun-version: 1.3.13 - uses: actions/cache@v5 with: @@ -41,7 +41,7 @@ jobs: restore-keys: ${{ runner.os }}-bun- - name: Install dependencies - run: bun install + run: bun install --frozen-lockfile - name: Type check run: bun run lint:tsc @@ -57,7 +57,7 @@ jobs: - uses: oven-sh/setup-bun@v2 with: - bun-version: "1.1.38" + bun-version: 1.3.13 - uses: actions/cache@v5 with: @@ -66,7 +66,7 @@ jobs: restore-keys: ${{ runner.os }}-bun- - name: Install dependencies - run: bun install + run: bun install --frozen-lockfile - name: Build plugin run: bun run build:plugin @@ -76,4 +76,3 @@ jobs: - name: Test plugin run: bun run test:plugin - diff --git a/.github/workflows/ci-flutter-iap.yml b/.github/workflows/ci-flutter-iap.yml index 5911a41b..698c0fe8 100644 --- a/.github/workflows/ci-flutter-iap.yml +++ b/.github/workflows/ci-flutter-iap.yml @@ -33,7 +33,7 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: "3.x" + flutter-version: "3.41.9" cache: true - name: Install dependencies diff --git a/.github/workflows/ci-maui-iap.yml b/.github/workflows/ci-maui-iap.yml index d7c76b55..a48ce58a 100644 --- a/.github/workflows/ci-maui-iap.yml +++ b/.github/workflows/ci-maui-iap.yml @@ -34,6 +34,11 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + XCODE_VERSION: 16.4 + XCODEGEN_VERSION: 2.45.4 + XCODEGEN_SHA256: 090ec29491aad50aec10631bf6e62253fed733c50f3aab0f5ffc86bc170bdbef + jobs: compile-check: name: Compile Check (net9.0 shared) @@ -115,13 +120,18 @@ jobs: ios-binding: name: iOS binding (net9.0-ios + maccatalyst) - runs-on: macos-latest + runs-on: macos-15 timeout-minutes: 45 steps: - uses: actions/checkout@v6 with: fetch-depth: 1 + - name: Set up Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.XCODE_VERSION }} + - name: Setup .NET 9 SDK uses: actions/setup-dotnet@v5 with: @@ -131,7 +141,7 @@ jobs: run: dotnet workload install maui --skip-sign-check - name: Install xcodegen - run: brew install xcodegen + run: bash scripts/install-xcodegen.sh "$XCODEGEN_VERSION" # The iOS binding csproj consumes packages/apple/.build/xcframework/OpenIAP.xcframework. # Build it via the wrapper Xcode project produced from packages/apple/wrapper/project.yml. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 042a9cba..e633774a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,9 @@ on: branches: - main +env: + XCODE_VERSION: 16.4 + jobs: # Detect which packages have changes changes: @@ -98,7 +101,7 @@ jobs: run: | # Retry bun install up to 3 times to handle transient registry errors for i in 1 2 3; do - bun install && break + bun install --frozen-lockfile && break [ $i -eq 3 ] && exit 1 echo "Attempt $i failed. Retrying..." sleep 5 @@ -169,7 +172,7 @@ jobs: run: | # Retry bun install up to 3 times to handle transient registry errors for i in 1 2 3; do - bun install && break + bun install --frozen-lockfile && break [ $i -eq 3 ] && exit 1 echo "Attempt $i failed. Retrying..." sleep 5 @@ -197,13 +200,18 @@ jobs: test-ios: name: Test iOS - runs-on: macos-latest + runs-on: macos-15 needs: changes if: needs.changes.outputs.ios == 'true' steps: - name: Checkout uses: actions/checkout@v6 + - name: Set up Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.XCODE_VERSION }} + - name: Setup Bun uses: oven-sh/setup-bun@v2 with: @@ -213,7 +221,7 @@ jobs: run: | # Retry bun install up to 3 times to handle transient registry errors for i in 1 2 3; do - bun install && break + bun install --frozen-lockfile && break [ $i -eq 3 ] && exit 1 echo "Attempt $i failed. Retrying..." sleep 5 @@ -249,7 +257,7 @@ jobs: run: | # Retry bun install up to 3 times to handle transient registry errors for i in 1 2 3; do - bun install && break + bun install --frozen-lockfile && break [ $i -eq 3 ] && exit 1 echo "Attempt $i failed. Retrying..." sleep 5 @@ -290,7 +298,7 @@ jobs: run: | # Retry bun install up to 3 times to handle transient registry errors for i in 1 2 3; do - bun install && break + bun install --frozen-lockfile && break [ $i -eq 3 ] && exit 1 echo "Attempt $i failed. Retrying..." sleep 5 diff --git a/.github/workflows/publish-flutter.yml b/.github/workflows/publish-flutter.yml index 39549029..f972f984 100644 --- a/.github/workflows/publish-flutter.yml +++ b/.github/workflows/publish-flutter.yml @@ -8,10 +8,12 @@ on: permissions: contents: write - id-token: write jobs: publish: + permissions: + contents: write + id-token: write runs-on: ubuntu-latest defaults: run: @@ -24,12 +26,34 @@ jobs: uses: subosito/flutter-action@v2 with: channel: "stable" - flutter-version: "3.x" + flutter-version: "3.41.9" - name: Install dependencies run: flutter pub get + - name: Check if pub.dev package already published + id: check_pub + run: | + VERSION=$(awk '/^version:/{print $2; exit}' pubspec.yaml) + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://pub.dev/api/packages/flutter_inapp_purchase/versions/$VERSION" || true) + HTTP_STATUS="${HTTP_STATUS:-000}" + case "$HTTP_STATUS" in + 200) + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "⚠️ flutter_inapp_purchase $VERSION already exists on pub.dev" + ;; + 404) + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "✓ flutter_inapp_purchase $VERSION does not exist, will publish" + ;; + *) + echo "❌ Unable to verify flutter_inapp_purchase $VERSION on pub.dev (HTTP $HTTP_STATUS)" + exit 1 + ;; + esac + - name: Publish to pub.dev + if: steps.check_pub.outputs.exists == 'false' run: | OIDC_TOKEN=$(curl -sLS "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=https://pub.dev" \ -H "User-Agent: actions/oidc-client" \ diff --git a/.github/workflows/release-apple.yml b/.github/workflows/release-apple.yml index ee494211..0b88ecbf 100644 --- a/.github/workflows/release-apple.yml +++ b/.github/workflows/release-apple.yml @@ -37,20 +37,25 @@ concurrency: group: ${{ github.workflow }} cancel-in-progress: false +env: + XCODE_VERSION: 16.4 + COCOAPODS_VERSION: 1.15.2 + permissions: contents: write jobs: validate-ios: - runs-on: macos-latest + runs-on: macos-15 timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v6 - - name: Select Xcode - run: | - sudo xcode-select -s /Applications/Xcode.app + - name: Set up Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.XCODE_VERSION }} - name: Build working-directory: packages/apple @@ -62,7 +67,7 @@ jobs: release: needs: [validate-ios] - runs-on: macos-latest + runs-on: macos-15 steps: - name: Checkout @@ -70,18 +75,25 @@ jobs: with: fetch-depth: 0 + - name: Set up Xcode + if: inputs.publish_cocoapods == true + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.XCODE_VERSION }} + - name: Calculate new version id: version + env: + TARGET_VERSION: ${{ github.event.inputs.target_version }} + VERSION_TYPE: ${{ github.event.inputs.version }} + IS_PRERELEASE: ${{ github.event.inputs.prerelease }} run: | # Read current apple version from openiap-versions.json CURRENT_VERSION=$(jq -r '.apple' openiap-versions.json) echo "Current version: $CURRENT_VERSION" echo "PREVIOUS_VERSION=$CURRENT_VERSION" >> $GITHUB_ENV echo "previous_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT - TARGET_VERSION="${{ github.event.inputs.target_version }}" - VERSION_TYPE="${{ github.event.inputs.version }}" - IS_PRERELEASE="${{ github.event.inputs.prerelease }}" BASE_VERSION="${CURRENT_VERSION%%-*}" IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE_VERSION" @@ -134,15 +146,31 @@ jobs: # Allow reusing existing semver tags (for reruns where CocoaPods succeeded but SPM failed) if git rev-parse "$VERSION" >/dev/null 2>&1; then echo "exists=true" >> $GITHUB_OUTPUT + echo "bare_exists=true" >> $GITHUB_OUTPUT echo "⚠️ Tag $VERSION already exists. Will reuse existing tag." elif git rev-parse "apple-v$VERSION" >/dev/null 2>&1; then echo "exists=true" >> $GITHUB_OUTPUT + echo "bare_exists=false" >> $GITHUB_OUTPUT echo "⚠️ Legacy tag apple-v$VERSION already exists. Will reuse existing tag." else echo "exists=false" >> $GITHUB_OUTPUT + echo "bare_exists=false" >> $GITHUB_OUTPUT echo "✓ Tag $VERSION does not exist, proceeding with release" fi + - name: Checkout release tag (current version) + if: steps.version.outputs.skip_version_commit == 'true' && steps.check_tag.outputs.exists == 'true' + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + RELEASE_TAG="$VERSION" + LEGACY_TAG="apple-v$VERSION" + if git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + git checkout "$RELEASE_TAG" + else + git checkout "$LEGACY_TAG" + fi + - name: Update version in openiap-versions.json if: steps.version.outputs.skip_version_commit != 'true' working-directory: packages/apple @@ -166,6 +194,7 @@ jobs: git config user.email "github-actions[bot]@users.noreply.github.com" git add openiap-versions.json packages/*/openiap-versions.json + git add packages/gql/package.json packages/docs/package.json packages/google/package.json packages/apple/package.json if git diff --staged --quiet; then echo "No version changes to commit" @@ -178,30 +207,28 @@ jobs: if ! git pull --rebase origin main; then echo "⚠️ Rebase conflict detected, auto-resolving version files..." - # Auto-resolve openiap-versions.json conflicts by applying only our - # key on top of theirs. Taking ours wholesale would regress other - # packages' versions when a concurrent release landed in between - # (race observed 2026-04-24: google bump regressed apple 2.1.3 → 2.1.2). + # Auto-resolve version metadata conflicts by applying only our + # key on top of upstream. Taking ours wholesale would regress + # other packages' versions when a concurrent release landed in + # between (race observed 2026-04-24: google bump regressed apple + # 2.1.3 → 2.1.2). for conflict_file in $(git diff --name-only --diff-filter=U); do - if [[ "$conflict_file" == *"openiap-versions.json" ]]; then - git show HEAD:"$conflict_file" > /tmp/theirs.json 2>/dev/null || true - - if [ -s /tmp/theirs.json ]; then - jq --arg version "$VERSION" '.apple = $version' /tmp/theirs.json > /tmp/merged.json - cp /tmp/merged.json "$conflict_file" - else - git checkout --theirs "$conflict_file" 2>/dev/null || git checkout --ours "$conflict_file" - fi - git add "$conflict_file" - else - echo "❌ Unexpected conflict in $conflict_file" - exit 1 - fi + case "$conflict_file" in + openiap-versions.json|packages/*/openiap-versions.json|packages/gql/package.json|packages/docs/package.json|packages/google/package.json|packages/apple/package.json) + ;; + *) + echo "❌ Unexpected conflict in $conflict_file" + exit 1 + ;; + esac done - # Also re-sync docs copy after merge - cp openiap-versions.json packages/docs/openiap-versions.json - git add packages/docs/openiap-versions.json + # Re-sync package metadata and docs copy after merge + git show HEAD:openiap-versions.json > /tmp/upstream-openiap-versions.json + jq --arg version "$VERSION" '.apple = $version' /tmp/upstream-openiap-versions.json > openiap-versions.json + ./scripts/sync-versions.sh + git add openiap-versions.json packages/*/openiap-versions.json + git add packages/gql/package.json packages/docs/package.json packages/google/package.json packages/apple/package.json GIT_EDITOR=true git rebase --continue || { echo "❌ Rebase continue failed"; exit 1; } fi @@ -247,7 +274,7 @@ jobs: - name: Install CocoaPods if: inputs.publish_cocoapods == true run: | - gem install cocoapods + gem install cocoapods -v "$COCOAPODS_VERSION" - name: Validate Podspec if: inputs.publish_cocoapods == true @@ -259,11 +286,11 @@ jobs: pod lib lint openiap.podspec --allow-warnings - name: Cleanup on validation failure - if: inputs.publish_cocoapods == true && failure() && steps.validate_podspec.outcome == 'failure' + if: inputs.publish_cocoapods == true && failure() && steps.validate_podspec.outcome == 'failure' && steps.check_tag.outputs.bare_exists != 'true' env: VERSION: ${{ steps.version.outputs.version }} run: | - echo "❌ Podspec validation failed. Cleaning up tag and commit..." + echo "❌ Podspec validation failed. Cleaning up tag created by this run..." # Delete the tag locally and remotely git tag -d "$VERSION" || true @@ -293,6 +320,11 @@ jobs: PREVIOUS_VERSION: ${{ steps.version.outputs.previous_version }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + RELEASE_REF="HEAD" + if git rev-parse "$VERSION" >/dev/null 2>&1; then + RELEASE_REF="$VERSION" + fi + # Get the previous apple tag PREV_TAG="" @@ -308,8 +340,8 @@ jobs: echo "No previous apple tag found" CHANGELOG="- Initial release" else - echo "Generating changelog from $PREV_TAG to HEAD" - CHANGELOG=$(git log $PREV_TAG..HEAD --pretty=format:"- %s ([\`%h\`](https://github.com/hyodotdev/openiap/commit/%H))" -- packages/apple/) + echo "Generating changelog from $PREV_TAG to $RELEASE_REF" + CHANGELOG=$(git log "$PREV_TAG..$RELEASE_REF" --pretty=format:"- %s ([\`%h\`](https://github.com/hyodotdev/openiap/commit/%H))" -- packages/apple/) if [ -z "$CHANGELOG" ]; then CHANGELOG="- No direct code changes in packages/apple/." @@ -347,8 +379,8 @@ jobs: ### Documentation - - [API reference](https://www.openiap.dev/docs/apis) - - [Release notes](https://www.openiap.dev/docs/updates/releases) + - [API reference](https://openiap.dev/docs/apis) + - [Release notes](https://openiap.dev/docs/updates/releases) - [GitHub (monorepo)](https://github.com/hyodotdev/openiap/tree/main/packages/apple) ### Links diff --git a/.github/workflows/release-expo.yml b/.github/workflows/release-expo.yml index 27530f59..a81ddf6d 100644 --- a/.github/workflows/release-expo.yml +++ b/.github/workflows/release-expo.yml @@ -19,19 +19,12 @@ on: required: false default: false type: boolean - create_release: - description: "Also create a GitHub Release" - required: false - default: true - type: boolean - concurrency: group: ${{ github.workflow }} cancel-in-progress: false permissions: contents: write - id-token: write jobs: validate-android: @@ -49,19 +42,19 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: 20.x + node-version: 20 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: "1.1.38" + bun-version: 1.3.13 - name: Install dependencies - run: bun install + run: bun install --frozen-lockfile - name: Install example dependencies working-directory: libraries/expo-iap/example - run: bun install + run: bun install --frozen-lockfile - name: Install JDK uses: actions/setup-java@v5 @@ -95,19 +88,19 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: 20.x + node-version: 20 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: "1.1.38" + bun-version: 1.3.13 - name: Install dependencies - run: bun install + run: bun install --frozen-lockfile - name: Install example dependencies working-directory: libraries/expo-iap/example - run: bun install + run: bun install --frozen-lockfile - name: Set up Xcode uses: maxim-lobanov/setup-xcode@v1 @@ -136,6 +129,9 @@ jobs: deploy: needs: [validate-android, validate-ios] + permissions: + contents: write + id-token: write runs-on: ubuntu-latest defaults: run: @@ -149,13 +145,24 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: 20.x + node-version: 20 registry-url: "https://registry.npmjs.org" - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: "1.1.38" + bun-version: 1.3.13 + + - name: Checkout release tag (current version) + if: ${{ inputs.version == 'current' }} + run: | + NEW_VERSION=$(node -p "require('./package.json').version") + RELEASE_TAG="expo-iap-${NEW_VERSION}" + if git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + git checkout "$RELEASE_TAG" + else + echo "Tag $RELEASE_TAG does not exist; current mode will use HEAD." + fi - name: Install dependencies run: bun install --frozen-lockfile @@ -202,24 +209,66 @@ jobs: echo "is_prerelease=false" >> "$GITHUB_OUTPUT" fi + - name: Check if release tag already exists + id: check_tag + env: + NEW_VERSION: ${{ steps.bump.outputs.version }} + VERSION_TYPE: ${{ inputs.version }} + run: | + RELEASE_TAG="expo-iap-${NEW_VERSION}" + if git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + if [ "$VERSION_TYPE" = "current" ]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "⚠️ Tag $RELEASE_TAG already exists. Will reuse existing tag (current mode)." + else + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "❌ Tag $RELEASE_TAG already exists. Use 'current' to retry this version." + exit 1 + fi + else + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "✓ Tag $RELEASE_TAG does not exist" + fi + - name: Prepare package (build + codegen) run: bun run prepare - - name: Commit and tag + - name: Commit version update if: ${{ inputs.version != 'current' }} run: | NEW_VERSION="${{ steps.bump.outputs.version }}" git add package.json git commit -m "chore(release): expo-iap ${NEW_VERSION}" - git tag -a "expo-iap-${NEW_VERSION}" -m "Release expo-iap-${NEW_VERSION}" - name: Push commit and tags if: ${{ inputs.version != 'current' }} + env: + NEW_VERSION: ${{ steps.bump.outputs.version }} run: | - git stash --include-untracked + STASHED=false + if ! git diff --quiet || [ -n "$(git ls-files --others --exclude-standard)" ]; then + git stash push --include-untracked -m "release artifacts" + STASHED=true + fi git pull --rebase origin main - git stash pop || true - git push --follow-tags + if [ "$STASHED" = "true" ]; then + git stash pop + fi + git tag -a "expo-iap-${NEW_VERSION}" -m "Release expo-iap-${NEW_VERSION}" + git push origin HEAD:main --follow-tags + + - name: Create tag (current version) + if: ${{ inputs.version == 'current' && steps.check_tag.outputs.exists != 'true' }} + env: + NEW_VERSION: ${{ steps.bump.outputs.version }} + run: | + TAG_TARGET="HEAD" + PUBLISHED_GIT_HEAD=$(npm view "expo-iap@$NEW_VERSION" gitHead 2>/dev/null || true) + if [ -n "$PUBLISHED_GIT_HEAD" ] && git cat-file -e "$PUBLISHED_GIT_HEAD^{commit}" 2>/dev/null; then + TAG_TARGET="$PUBLISHED_GIT_HEAD" + fi + git tag -a "expo-iap-${NEW_VERSION}" "$TAG_TARGET" -m "Release expo-iap-${NEW_VERSION}" + git push origin "expo-iap-${NEW_VERSION}" - name: Resolve symlinks for npm publish run: | @@ -229,9 +278,27 @@ jobs: fi - name: Ensure npm CLI v11.5.1 or later (required for OIDC) - run: npm install -g npm@latest + run: npm install -g npm@11.5.1 + + - name: Check if npm package already published + id: check_npm + env: + VERSION: ${{ steps.bump.outputs.version }} + run: | + if NPM_OUTPUT=$(npm view "expo-iap@$VERSION" version 2>&1); then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "⚠️ expo-iap@$VERSION already exists on npm" + elif echo "$NPM_OUTPUT" | grep -qiE 'E404|404 Not Found'; then + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "✓ expo-iap@$VERSION does not exist, will publish" + else + echo "❌ Unable to verify expo-iap@$VERSION on npm" + echo "$NPM_OUTPUT" + exit 1 + fi - name: Publish to npm with OIDC trusted publishing + if: steps.check_npm.outputs.exists == 'false' run: | if [ "${{ steps.bump.outputs.is_prerelease }}" = "true" ]; then npm publish --tag next --access public --provenance @@ -240,14 +307,17 @@ jobs: fi - name: Generate release notes - if: ${{ inputs.create_release }} id: release_notes env: VERSION: ${{ steps.bump.outputs.version }} run: | + RELEASE_REF="HEAD" + if git rev-parse "expo-iap-$VERSION" >/dev/null 2>&1; then + RELEASE_REF="expo-iap-$VERSION" + fi PREV_TAG=$(git for-each-ref --sort=-creatordate --format '%(refname:short)' 'refs/tags/expo-iap-*' | grep -v "$VERSION" | head -n 1) if [ -n "$PREV_TAG" ]; then - CHANGELOG=$(git log "$PREV_TAG..HEAD" --pretty=format:"- %s ([\`%h\`](https://github.com/hyodotdev/openiap/commit/%H))" -- libraries/expo-iap/ packages/google/ packages/apple/) + CHANGELOG=$(git log "$PREV_TAG..$RELEASE_REF" --pretty=format:"- %s ([\`%h\`](https://github.com/hyodotdev/openiap/commit/%H))" -- libraries/expo-iap/ packages/google/ packages/apple/) fi if [ -z "$CHANGELOG" ]; then CHANGELOG="- No direct code changes — picks up the latest openiap-google / openiap-apple native library updates. See the consolidated release notes for details." @@ -271,9 +341,9 @@ jobs: ### Documentation - - [Setup guide — Expo](https://www.openiap.dev/docs/setup/expo) - - [API reference](https://www.openiap.dev/docs/apis) - - [Release notes](https://www.openiap.dev/docs/updates/releases) + - [Setup guide — Expo](https://openiap.dev/docs/setup/expo) + - [API reference](https://openiap.dev/docs/apis) + - [Release notes](https://openiap.dev/docs/updates/releases) ### Links @@ -282,7 +352,6 @@ jobs: EOF - name: Create GitHub Release - if: ${{ inputs.create_release }} uses: softprops/action-gh-release@v3 with: tag_name: expo-iap-${{ steps.bump.outputs.version }} diff --git a/.github/workflows/release-flutter.yml b/.github/workflows/release-flutter.yml index a59196ce..38fe6bd7 100644 --- a/.github/workflows/release-flutter.yml +++ b/.github/workflows/release-flutter.yml @@ -19,19 +19,12 @@ on: required: false default: false type: boolean - create_release: - description: "Also create a GitHub Release" - required: false - default: true - type: boolean - concurrency: group: ${{ github.workflow }} cancel-in-progress: false permissions: contents: write - id-token: write jobs: validate-android: @@ -49,7 +42,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: "stable" - flutter-version: "3.x" + flutter-version: "3.41.9" - name: Install dependencies run: flutter pub get @@ -91,7 +84,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: "stable" - flutter-version: "3.x" + flutter-version: "3.41.9" - name: Set up Xcode uses: maxim-lobanov/setup-xcode@v1 @@ -128,6 +121,9 @@ jobs: deploy: needs: [validate-android, validate-ios] if: github.ref == 'refs/heads/main' + permissions: + contents: write + id-token: write runs-on: ubuntu-latest defaults: run: @@ -143,11 +139,31 @@ jobs: uses: subosito/flutter-action@v2 with: channel: "stable" - flutter-version: "3.x" + flutter-version: "3.41.9" + + - name: Checkout release tag (current version) + if: ${{ inputs.version == 'current' }} + id: current_tag + run: | + NEW_VERSION=$(awk '/^version:/{print $2; exit}' pubspec.yaml) + RELEASE_TAG="flutter-iap-${NEW_VERSION}" + if git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + git checkout "$RELEASE_TAG" + echo "checked_out=true" >> "$GITHUB_OUTPUT" + else + echo "checked_out=false" >> "$GITHUB_OUTPUT" + echo "Tag $RELEASE_TAG does not exist; current mode will use HEAD." + fi - name: Install dependencies run: flutter pub get + - name: Validate checked-out current tag + if: ${{ steps.current_tag.outputs.checked_out == 'true' }} + run: | + flutter analyze + flutter test + - name: Configure Git user run: | git config user.name "flutter-iap bot" @@ -214,6 +230,27 @@ jobs: echo "is_prerelease=false" >> "$GITHUB_OUTPUT" fi + - name: Check if release tag already exists + id: check_tag + env: + NEW_VERSION: ${{ steps.bump.outputs.version }} + VERSION_TYPE: ${{ inputs.version }} + run: | + RELEASE_TAG="flutter-iap-${NEW_VERSION}" + if git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + if [ "$VERSION_TYPE" = "current" ]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "⚠️ Tag $RELEASE_TAG already exists. Will reuse existing tag (current mode)." + else + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "❌ Tag $RELEASE_TAG already exists. Use 'current' to retry this version." + exit 1 + fi + else + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "✓ Tag $RELEASE_TAG does not exist" + fi + - name: Generate CHANGELOG entry if: ${{ inputs.version != 'current' }} env: @@ -246,7 +283,7 @@ jobs: printf "%b" "# Changelog\n\n${ENTRY}" > CHANGELOG.md fi - - name: Commit and tag + - name: Commit version update if: ${{ inputs.version != 'current' }} env: NEW_VERSION: ${{ steps.bump.outputs.version }} @@ -254,39 +291,50 @@ jobs: git add pubspec.yaml [ -f CHANGELOG.md ] && git add CHANGELOG.md git commit -m "chore(release): flutter_inapp_purchase ${NEW_VERSION}" - git tag -a "flutter-iap-${NEW_VERSION}" -m "Release flutter-iap-${NEW_VERSION}" - name: Tag only (current version) - if: ${{ inputs.version == 'current' }} + if: ${{ inputs.version == 'current' && steps.check_tag.outputs.exists != 'true' }} env: NEW_VERSION: ${{ steps.bump.outputs.version }} run: | - git tag -af "flutter-iap-${NEW_VERSION}" -m "Release flutter-iap-${NEW_VERSION}" + git tag -a "flutter-iap-${NEW_VERSION}" -m "Release flutter-iap-${NEW_VERSION}" - name: Push commit and tags if: ${{ inputs.version != 'current' }} + env: + NEW_VERSION: ${{ steps.bump.outputs.version }} run: | - git stash --include-untracked || true + STASHED=false + if ! git diff --quiet || [ -n "$(git ls-files --others --exclude-standard)" ]; then + git stash push --include-untracked -m "release artifacts" + STASHED=true + fi git pull --rebase origin main - git stash pop || true - git push --follow-tags + if [ "$STASHED" = "true" ]; then + git stash pop + fi + git tag -a "flutter-iap-${NEW_VERSION}" -m "Release flutter-iap-${NEW_VERSION}" + git push origin HEAD:main --follow-tags - name: Push tag (current version) - if: ${{ inputs.version == 'current' }} + if: ${{ inputs.version == 'current' && steps.check_tag.outputs.exists != 'true' }} env: NEW_VERSION: ${{ steps.bump.outputs.version }} run: | - git push origin "flutter-iap-${NEW_VERSION}" --force + git push origin "flutter-iap-${NEW_VERSION}" - name: Generate release notes - if: ${{ inputs.create_release }} id: release_notes env: VERSION: ${{ steps.bump.outputs.version }} run: | + RELEASE_REF="HEAD" + if git rev-parse "flutter-iap-$VERSION" >/dev/null 2>&1; then + RELEASE_REF="flutter-iap-$VERSION" + fi PREV_TAG=$(git for-each-ref --sort=-creatordate --format '%(refname:short)' 'refs/tags/flutter-iap-*' | grep -v "$VERSION" | head -n 1) if [ -n "$PREV_TAG" ]; then - CHANGELOG=$(git log "$PREV_TAG..HEAD" --pretty=format:"- %s ([\`%h\`](https://github.com/hyodotdev/openiap/commit/%H))" -- libraries/flutter_inapp_purchase/ packages/google/ packages/apple/) + CHANGELOG=$(git log "$PREV_TAG..$RELEASE_REF" --pretty=format:"- %s ([\`%h\`](https://github.com/hyodotdev/openiap/commit/%H))" -- libraries/flutter_inapp_purchase/ packages/google/ packages/apple/) fi if [ -z "$CHANGELOG" ]; then CHANGELOG="- No direct code changes — picks up the latest openiap-google / openiap-apple native library updates. See the consolidated release notes for details." @@ -314,9 +362,9 @@ jobs: ### Documentation - - [Setup guide — Flutter](https://www.openiap.dev/docs/setup/flutter) - - [API reference](https://www.openiap.dev/docs/apis) - - [Release notes](https://www.openiap.dev/docs/updates/releases) + - [Setup guide — Flutter](https://openiap.dev/docs/setup/flutter) + - [API reference](https://openiap.dev/docs/apis) + - [Release notes](https://openiap.dev/docs/updates/releases) ### Links @@ -325,7 +373,6 @@ jobs: EOF - name: Create GitHub Release - if: ${{ inputs.create_release }} uses: softprops/action-gh-release@v3 with: tag_name: flutter-iap-${{ steps.bump.outputs.version }} diff --git a/.github/workflows/release-godot.yml b/.github/workflows/release-godot.yml index aba60b4b..8c129589 100644 --- a/.github/workflows/release-godot.yml +++ b/.github/workflows/release-godot.yml @@ -19,11 +19,6 @@ on: required: false default: false type: boolean - create_release: - description: "Also create a GitHub Release" - required: false - default: true - type: boolean notarize_macos: description: "Include notarized macOS runtime frameworks" required: false @@ -218,6 +213,35 @@ jobs: echo "is_prerelease=false" >> "$GITHUB_OUTPUT" fi + - name: Check if release tag already exists + id: check_tag + env: + VERSION: ${{ steps.version.outputs.VERSION }} + VERSION_TYPE: ${{ inputs.version }} + run: | + RELEASE_TAG="godot-iap-${VERSION}" + if git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + if [ "$VERSION_TYPE" = "current" ]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "⚠️ Tag $RELEASE_TAG already exists. Will reuse existing tag (current mode)." + else + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "❌ Tag $RELEASE_TAG already exists. Use 'current' to retry this version." + exit 1 + fi + else + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "✓ Tag $RELEASE_TAG does not exist" + fi + + - name: Checkout release tag (current version) + if: ${{ inputs.version == 'current' && steps.check_tag.outputs.exists == 'true' }} + env: + VERSION: ${{ steps.version.outputs.VERSION }} + run: | + RELEASE_TAG="godot-iap-${VERSION}" + git checkout "$RELEASE_TAG" + - name: Update version in plugin.cfg if: ${{ inputs.version != 'current' }} run: | @@ -254,17 +278,7 @@ jobs: cp android/build/outputs/aar/godot-iap-release.aar dist/addons/godot-iap/android/GodotIap.release.aar cp android/build/outputs/aar/godot-iap-release.aar dist/addons/godot-iap/android/GodotIap.debug.aar - OPENIAP_GOOGLE_VERSION=$(cat openiap-versions.json | grep '"google"' | cut -d'"' -f4) - cat > dist/addons/godot-iap/android/GodotIap.gdap << EOF - [config] - name="GodotIap" - binary_type="local" - binary="GodotIap.release.aar" - - [dependencies] - local=[] - remote=["com.android.billingclient:billing:7.1.1", "io.github.hyochan.openiap:openiap-google:${OPENIAP_GOOGLE_VERSION}"] - EOF + ./scripts/write-gdap.sh dist/addons/godot-iap/android/GodotIap.gdap cp -R addons/godot-iap/bin/ios/GodotIap.framework dist/addons/godot-iap/bin/ios/ cp -R addons/godot-iap/bin/ios/SwiftGodotRuntime.framework dist/addons/godot-iap/bin/ios/ @@ -389,28 +403,43 @@ jobs: fi fi - - name: Commit and tag + - name: Commit version update and tag if: ${{ inputs.version != 'current' }} run: | git add addons/godot-iap/plugin.cfg if ! git diff --cached --quiet; then git commit -m "chore(release): godot-iap ${{ steps.version.outputs.VERSION }}" fi - git tag -af godot-iap-${{ steps.version.outputs.VERSION }} -m "Release godot-iap-${{ steps.version.outputs.VERSION }}" - git stash --include-untracked || true + STASHED=false + if ! git diff --quiet || [ -n "$(git ls-files --others --exclude-standard)" ]; then + git stash push --include-untracked -m "release artifacts" + STASHED=true + fi git pull --rebase origin main - git stash pop || true - git push --follow-tags + if [ "$STASHED" = "true" ]; then + git stash pop + fi + git tag -a godot-iap-${{ steps.version.outputs.VERSION }} -m "Release godot-iap-${{ steps.version.outputs.VERSION }}" + git push origin HEAD:main --follow-tags + + - name: Create tag (current version) + if: ${{ inputs.version == 'current' && steps.check_tag.outputs.exists != 'true' }} + run: | + git tag -a godot-iap-${{ steps.version.outputs.VERSION }} -m "Release godot-iap-${{ steps.version.outputs.VERSION }}" + git push origin godot-iap-${{ steps.version.outputs.VERSION }} - name: Generate release notes - if: ${{ inputs.create_release }} id: release_notes env: VERSION: ${{ steps.version.outputs.VERSION }} run: | + RELEASE_REF="HEAD" + if git rev-parse "godot-iap-$VERSION" >/dev/null 2>&1; then + RELEASE_REF="godot-iap-$VERSION" + fi PREV_TAG=$(git for-each-ref --sort=-creatordate --format '%(refname:short)' 'refs/tags/godot-iap-*' | grep -v "$VERSION" | head -n 1) if [ -n "$PREV_TAG" ]; then - CHANGELOG=$(git log "$PREV_TAG..HEAD" --pretty=format:"- %s ([\`%h\`](https://github.com/hyodotdev/openiap/commit/%H))" -- libraries/godot-iap/ packages/google/) + CHANGELOG=$(git log "$PREV_TAG..$RELEASE_REF" --pretty=format:"- %s ([\`%h\`](https://github.com/hyodotdev/openiap/commit/%H))" -- libraries/godot-iap/ packages/google/) fi if [ -z "$CHANGELOG" ]; then CHANGELOG="- No direct code changes — picks up the latest openiap-google native library updates. See the consolidated release notes for details." @@ -432,9 +461,9 @@ jobs: ### Documentation - - [Setup guide — Godot](https://www.openiap.dev/docs/setup/godot) - - [API reference](https://www.openiap.dev/docs/apis) - - [Release notes](https://www.openiap.dev/docs/updates/releases) + - [Setup guide — Godot](https://openiap.dev/docs/setup/godot) + - [API reference](https://openiap.dev/docs/apis) + - [Release notes](https://openiap.dev/docs/updates/releases) ### Links @@ -443,7 +472,6 @@ jobs: EOF - name: Create Release - if: ${{ inputs.create_release }} uses: softprops/action-gh-release@v3 with: tag_name: godot-iap-${{ steps.version.outputs.VERSION }} diff --git a/.github/workflows/release-google.yml b/.github/workflows/release-google.yml index fed06f06..04d3445a 100644 --- a/.github/workflows/release-google.yml +++ b/.github/workflows/release-google.yml @@ -60,7 +60,7 @@ jobs: release: needs: [validate-android] - runs-on: macos-latest + runs-on: ubuntu-latest steps: - name: Checkout @@ -70,13 +70,14 @@ jobs: - name: Calculate new version id: version + env: + VERSION_TYPE: ${{ github.event.inputs.version }} + IS_PRERELEASE: ${{ github.event.inputs.prerelease }} run: | # Read current google version from openiap-versions.json CURRENT_VERSION=$(jq -r '.google' openiap-versions.json) echo "Current version: $CURRENT_VERSION" - VERSION_TYPE="${{ github.event.inputs.version }}" - IS_PRERELEASE="${{ github.event.inputs.prerelease }}" BASE_VERSION="${CURRENT_VERSION%%-*}" IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE_VERSION" @@ -143,6 +144,19 @@ jobs: echo "✓ Tag google-$VERSION does not exist, proceeding with release" fi + - name: Checkout release tag (current version) + if: steps.version.outputs.skip_version_commit == 'true' && steps.check_tag.outputs.exists == 'true' + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + RELEASE_TAG="google-$VERSION" + LEGACY_TAG="google-v$VERSION" + if git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + git checkout "$RELEASE_TAG" + else + git checkout "$LEGACY_TAG" + fi + - name: Update version in openiap-versions.json if: steps.version.outputs.skip_version_commit != 'true' working-directory: packages/google @@ -166,6 +180,7 @@ jobs: git config user.email "github-actions[bot]@users.noreply.github.com" git add openiap-versions.json packages/*/openiap-versions.json + git add packages/gql/package.json packages/docs/package.json packages/google/package.json packages/apple/package.json if git diff --staged --quiet; then echo "No version changes to commit" @@ -178,30 +193,28 @@ jobs: if ! git pull --rebase origin main; then echo "⚠️ Rebase conflict detected, auto-resolving version files..." - # Auto-resolve openiap-versions.json conflicts by applying only our - # key on top of theirs. Taking ours wholesale would regress other - # packages' versions when a concurrent release landed in between - # (race observed 2026-04-24: google bump regressed apple 2.1.3 → 2.1.2). + # Auto-resolve version metadata conflicts by applying only our + # key on top of upstream. Taking ours wholesale would regress + # other packages' versions when a concurrent release landed in + # between (race observed 2026-04-24: google bump regressed apple + # 2.1.3 → 2.1.2). for conflict_file in $(git diff --name-only --diff-filter=U); do - if [[ "$conflict_file" == *"openiap-versions.json" ]]; then - git show HEAD:"$conflict_file" > /tmp/theirs.json 2>/dev/null || true - - if [ -s /tmp/theirs.json ]; then - jq --arg version "$VERSION" '.google = $version' /tmp/theirs.json > /tmp/merged.json - cp /tmp/merged.json "$conflict_file" - else - git checkout --theirs "$conflict_file" 2>/dev/null || git checkout --ours "$conflict_file" - fi - git add "$conflict_file" - else - echo "❌ Unexpected conflict in $conflict_file" - exit 1 - fi + case "$conflict_file" in + openiap-versions.json|packages/*/openiap-versions.json|packages/gql/package.json|packages/docs/package.json|packages/google/package.json|packages/apple/package.json) + ;; + *) + echo "❌ Unexpected conflict in $conflict_file" + exit 1 + ;; + esac done - # Also re-sync docs copy after merge - cp openiap-versions.json packages/docs/openiap-versions.json - git add packages/docs/openiap-versions.json + # Re-sync package metadata and docs copy after merge + git show HEAD:openiap-versions.json > /tmp/upstream-openiap-versions.json + jq --arg version "$VERSION" '.google = $version' /tmp/upstream-openiap-versions.json > openiap-versions.json + ./scripts/sync-versions.sh + git add openiap-versions.json packages/*/openiap-versions.json + git add packages/gql/package.json packages/docs/package.json packages/google/package.json packages/apple/package.json GIT_EDITOR=true git rebase --continue || { echo "❌ Rebase continue failed"; exit 1; } fi @@ -241,13 +254,22 @@ jobs: env: VERSION: ${{ steps.version.outputs.version }} run: | - if curl -s "https://repo1.maven.org/maven2/io/github/hyochan/openiap/openiap-google-horizon/$VERSION/" | grep -q "$VERSION"; then - echo "exists=true" >> $GITHUB_OUTPUT - echo "⚠️ openiap-google-horizon $VERSION already exists on Maven Central" - else - echo "exists=false" >> $GITHUB_OUTPUT - echo "✓ openiap-google-horizon $VERSION does not exist, will publish" - fi + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://repo1.maven.org/maven2/io/github/hyochan/openiap/openiap-google-horizon/$VERSION/" || true) + HTTP_STATUS="${HTTP_STATUS:-000}" + case "$HTTP_STATUS" in + 200) + echo "exists=true" >> $GITHUB_OUTPUT + echo "⚠️ openiap-google-horizon $VERSION already exists on Maven Central" + ;; + 404) + echo "exists=false" >> $GITHUB_OUTPUT + echo "✓ openiap-google-horizon $VERSION does not exist, will publish" + ;; + *) + echo "❌ Unable to verify openiap-google-horizon $VERSION on Maven Central (HTTP $HTTP_STATUS)" + exit 1 + ;; + esac - name: Publish Horizon flavor to Maven Central if: steps.check_horizon.outputs.exists == 'false' @@ -273,13 +295,22 @@ jobs: env: VERSION: ${{ steps.version.outputs.version }} run: | - if curl -s "https://repo1.maven.org/maven2/io/github/hyochan/openiap/openiap-google/$VERSION/" | grep -q "$VERSION"; then - echo "exists=true" >> $GITHUB_OUTPUT - echo "⚠️ openiap-google $VERSION already exists on Maven Central" - else - echo "exists=false" >> $GITHUB_OUTPUT - echo "✓ openiap-google $VERSION does not exist, will publish" - fi + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://repo1.maven.org/maven2/io/github/hyochan/openiap/openiap-google/$VERSION/" || true) + HTTP_STATUS="${HTTP_STATUS:-000}" + case "$HTTP_STATUS" in + 200) + echo "exists=true" >> $GITHUB_OUTPUT + echo "⚠️ openiap-google $VERSION already exists on Maven Central" + ;; + 404) + echo "exists=false" >> $GITHUB_OUTPUT + echo "✓ openiap-google $VERSION does not exist, will publish" + ;; + *) + echo "❌ Unable to verify openiap-google $VERSION on Maven Central (HTTP $HTTP_STATUS)" + exit 1 + ;; + esac - name: Publish Play flavor to Maven Central if: steps.check_play.outputs.exists == 'false' @@ -300,16 +331,23 @@ jobs: echo "✅ Published openiap-google (Play flavor) to Maven Central" fi + - name: Build release artifacts + working-directory: packages/google + run: ./gradlew :openiap:assembleRelease --no-daemon --stacktrace + - name: Create release artifacts working-directory: packages/google run: | mkdir -p release-artifacts - cp openiap/build/outputs/aar/*.aar release-artifacts/ 2>/dev/null || echo "No AAR files found" - cp openiap/build/libs/*.jar release-artifacts/ 2>/dev/null || echo "No JAR files found" - if [ -d release-artifacts ] && [ "$(ls -A release-artifacts)" ]; then - (cd release-artifacts && shasum -a 256 * > ../checksums.txt) || true - zip -r release-artifacts.zip release-artifacts/ + shopt -s nullglob + artifacts=(openiap/build/outputs/aar/*.aar openiap/build/libs/*.jar) + if [ ${#artifacts[@]} -eq 0 ]; then + echo "::error::No Google release artifacts found" + exit 1 fi + cp "${artifacts[@]}" release-artifacts/ + (cd release-artifacts && shasum -a 256 * > ../checksums.txt) + zip -r release-artifacts.zip release-artifacts/ - name: Generate release notes id: release_notes @@ -317,15 +355,19 @@ jobs: VERSION: ${{ steps.version.outputs.version }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - # Get the previous google tag BEFORE creating the new one - PREV_TAG=$(git for-each-ref --sort=-creatordate --format '%(refname:short)' 'refs/tags/google-*' | head -n 1) + RELEASE_REF="HEAD" + if git rev-parse "google-$VERSION" >/dev/null 2>&1; then + RELEASE_REF="google-$VERSION" + fi + + PREV_TAG=$(git for-each-ref --sort=-creatordate --format '%(refname:short)' 'refs/tags/google-*' | grep -v "$VERSION" | head -n 1) if [ -z "$PREV_TAG" ]; then echo "No previous google tag found" CHANGELOG="- Initial release" else - echo "Generating changelog from $PREV_TAG to HEAD" - CHANGELOG=$(git log $PREV_TAG..HEAD --pretty=format:"- %s ([\`%h\`](https://github.com/hyodotdev/openiap/commit/%H))" -- packages/google/) + echo "Generating changelog from $PREV_TAG to $RELEASE_REF" + CHANGELOG=$(git log "$PREV_TAG..$RELEASE_REF" --pretty=format:"- %s ([\`%h\`](https://github.com/hyodotdev/openiap/commit/%H))" -- packages/google/) if [ -z "$CHANGELOG" ]; then CHANGELOG="- No direct code changes in packages/google/." @@ -385,8 +427,8 @@ jobs: ### Documentation - - [API reference](https://www.openiap.dev/docs/apis) - - [Release notes](https://www.openiap.dev/docs/updates/releases) + - [API reference](https://openiap.dev/docs/apis) + - [Release notes](https://openiap.dev/docs/updates/releases) - [GitHub (monorepo)](https://github.com/hyodotdev/openiap/tree/main/packages/google) ### Maven Central @@ -430,8 +472,19 @@ jobs: PRERELEASE_FLAG="--prerelease" fi - gh release create "google-$VERSION" \ - $ARTIFACTS \ - --title "Google $VERSION" \ - $PRERELEASE_FLAG \ - --notes-file /tmp/release-notes.md + if gh release view "google-$VERSION" >/dev/null 2>&1; then + echo "ℹ️ Release google-$VERSION already exists. Updating notes and artifacts." + gh release edit "google-$VERSION" \ + --title "Google $VERSION" \ + $PRERELEASE_FLAG \ + --notes-file /tmp/release-notes.md + if [ -n "$ARTIFACTS" ]; then + gh release upload "google-$VERSION" $ARTIFACTS --clobber + fi + else + gh release create "google-$VERSION" \ + $ARTIFACTS \ + --title "Google $VERSION" \ + $PRERELEASE_FLAG \ + --notes-file /tmp/release-notes.md + fi diff --git a/.github/workflows/release-kmp.yml b/.github/workflows/release-kmp.yml index 6eeb412a..9c40d4c6 100644 --- a/.github/workflows/release-kmp.yml +++ b/.github/workflows/release-kmp.yml @@ -19,18 +19,13 @@ on: required: false default: false type: boolean - create_release: - description: "Also create a GitHub Release" - required: false - default: true - type: boolean - concurrency: group: ${{ github.workflow }} cancel-in-progress: false env: GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx4g -XX:+UseParallelGC" + XCODE_VERSION: 16.4 permissions: contents: write @@ -61,7 +56,7 @@ jobs: run: ./gradlew :library:compileDebugKotlinAndroid --no-daemon --stacktrace validate-ios: - runs-on: macos-latest + runs-on: macos-15 timeout-minutes: 60 defaults: run: @@ -69,6 +64,11 @@ jobs: steps: - uses: actions/checkout@v6 + - name: Set up Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.XCODE_VERSION }} + - name: Set up JDK 17 uses: actions/setup-java@v5 with: @@ -89,7 +89,6 @@ jobs: - name: Build iOS frameworks run: | ./gradlew :library:podGenIos - ./gradlew :library:updatePodspecDependency ./gradlew :library:podInstallSyntheticIos ./gradlew :library:podBuildOpeniapIos ./gradlew :library:linkPodDebugFrameworkIosSimulatorArm64 @@ -97,7 +96,7 @@ jobs: publish: needs: [validate-android, validate-ios] - runs-on: macos-latest + runs-on: macos-15 defaults: run: working-directory: libraries/kmp-iap @@ -106,6 +105,11 @@ jobs: with: fetch-depth: 0 + - name: Set up Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.XCODE_VERSION }} + - name: Set up JDK 17 uses: actions/setup-java@v5 with: @@ -175,36 +179,57 @@ jobs: else echo "is_prerelease=false" >> "$GITHUB_OUTPUT" fi - sed -i '' "s/^libraryVersion=.*/libraryVersion=$VERSION/" gradle.properties - - name: Update version in gradle.properties - if: ${{ inputs.version != 'current' }} + - name: Check if release tag already exists + id: check_tag + env: + VERSION: ${{ steps.version.outputs.VERSION }} + VERSION_TYPE: ${{ inputs.version }} run: | - sed -i '' "s/^libraryVersion=.*/libraryVersion=${{ env.VERSION }}/" gradle.properties - - - name: Regenerate podspec with correct version - if: ${{ inputs.version != 'current' }} - run: ./gradlew :library:podspec + RELEASE_TAG="kmp-iap-$VERSION" + if git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + if [ "$VERSION_TYPE" = "current" ]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "⚠️ Tag $RELEASE_TAG already exists. Will reuse existing tag (current mode)." + else + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "❌ Tag $RELEASE_TAG already exists. Use 'current' to retry this version." + exit 1 + fi + else + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "✓ Tag $RELEASE_TAG does not exist" + fi - - name: Update podspec to read openiap-versions.json dynamically - if: ${{ inputs.version != 'current' }} - run: ./gradlew :library:updatePodspecDependency + - name: Checkout release tag (current version) + if: ${{ inputs.version == 'current' && steps.check_tag.outputs.exists == 'true' }} + env: + VERSION: ${{ steps.version.outputs.VERSION }} + run: | + RELEASE_TAG="kmp-iap-$VERSION" + git checkout "$RELEASE_TAG" - - name: Update README version + - name: Update release metadata if: ${{ inputs.version != 'current' }} - run: ./gradlew :library:updateReadmeVersion + env: + VERSION: ${{ env.VERSION }} + run: | + perl -0pi -e 's/^libraryVersion=.*/libraryVersion=$ENV{VERSION}/m' gradle.properties + ./scripts/update-readme-version.sh "$VERSION" - name: Commit version updates if: ${{ inputs.version != 'current' }} run: | git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" - git add README.md library/library.podspec gradle.properties - git diff --staged --quiet || git commit -m "chore(release): kmp-iap ${{ env.VERSION }}" - git stash --include-untracked || true - git pull --rebase origin main || true - git stash pop || true - git push || echo "No changes to commit" + git add gradle.properties + if git diff --staged --quiet; then + echo "No version changes to commit" + else + git commit -m "chore(release): kmp-iap ${{ env.VERSION }}" + git pull --rebase origin main + git push origin HEAD:main + fi - name: Clean build artifacts run: rm -rf library/build/cocoapods @@ -212,13 +237,35 @@ jobs: - name: Build iOS frameworks run: | ./gradlew :library:podGenIos - ./gradlew :library:updatePodspecDependency ./gradlew :library:podInstallSyntheticIos ./gradlew :library:podBuildOpeniapIos ./gradlew :library:linkPodDebugFrameworkIosSimulatorArm64 ./gradlew :library:linkPodDebugFrameworkIosArm64 + - name: Check if KMP package already published + id: check_maven + env: + VERSION: ${{ steps.version.outputs.VERSION }} + run: | + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://repo1.maven.org/maven2/io/github/hyochan/kmp-iap/$VERSION/" || true) + HTTP_STATUS="${HTTP_STATUS:-000}" + case "$HTTP_STATUS" in + 200) + echo "exists=true" >> $GITHUB_OUTPUT + echo "⚠️ kmp-iap $VERSION already exists on Maven Central" + ;; + 404) + echo "exists=false" >> $GITHUB_OUTPUT + echo "✓ kmp-iap $VERSION does not exist, will publish" + ;; + *) + echo "❌ Unable to verify kmp-iap $VERSION on Maven Central (HTTP $HTTP_STATUS)" + exit 1 + ;; + esac + - name: Publish to Maven Central + if: steps.check_maven.outputs.exists == 'false' env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} @@ -227,14 +274,33 @@ jobs: ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} run: ./gradlew :library:publishAndReleaseToMavenCentral --no-daemon --no-parallel + - name: Create and push tag + if: success() + env: + VERSION: ${{ steps.version.outputs.VERSION }} + run: | + RELEASE_TAG="kmp-iap-$VERSION" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + if git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + echo "Tag $RELEASE_TAG already exists" + else + git tag -a "$RELEASE_TAG" -m "Release $RELEASE_TAG" + git push origin "$RELEASE_TAG" + fi + - name: Generate release notes - if: inputs.create_release && success() + if: success() id: release_notes run: | VERSION="${{ env.VERSION }}" + RELEASE_REF="HEAD" + if git rev-parse "kmp-iap-$VERSION" >/dev/null 2>&1; then + RELEASE_REF="kmp-iap-$VERSION" + fi PREV_TAG=$(git for-each-ref --sort=-creatordate --format '%(refname:short)' 'refs/tags/kmp-iap-*' | grep -v "$VERSION" | head -n 1) if [ -n "$PREV_TAG" ]; then - CHANGELOG=$(git log "$PREV_TAG..HEAD" --pretty=format:"- %s ([\`%h\`](https://github.com/hyodotdev/openiap/commit/%H))" -- libraries/kmp-iap/ packages/google/ packages/apple/) + CHANGELOG=$(git log "$PREV_TAG..$RELEASE_REF" --pretty=format:"- %s ([\`%h\`](https://github.com/hyodotdev/openiap/commit/%H))" -- libraries/kmp-iap/ packages/google/ packages/apple/) fi if [ -z "$CHANGELOG" ]; then CHANGELOG="- No direct code changes — picks up the latest openiap-google / openiap-apple native library updates. See the consolidated release notes for details." @@ -254,7 +320,7 @@ jobs: \`\`\`kotlin dependencies { - implementation("io.github.hyochan.openiap:kmp-iap:$VERSION") + implementation("io.github.hyochan:kmp-iap:$VERSION") } \`\`\` @@ -262,44 +328,46 @@ jobs: \`\`\`groovy dependencies { - implementation 'io.github.hyochan.openiap:kmp-iap:$VERSION' + implementation 'io.github.hyochan:kmp-iap:$VERSION' } \`\`\` ### Documentation - - [Setup guide — Kotlin Multiplatform](https://www.openiap.dev/docs/setup/kmp) - - [API reference](https://www.openiap.dev/docs/apis) - - [Release notes](https://www.openiap.dev/docs/updates/releases) + - [Setup guide — Kotlin Multiplatform](https://openiap.dev/docs/setup/kmp) + - [API reference](https://openiap.dev/docs/apis) + - [Release notes](https://openiap.dev/docs/updates/releases) ### Links - - [Maven Central](https://central.sonatype.com/artifact/io.github.hyochan.openiap/kmp-iap/$VERSION) + - [Maven Central](https://central.sonatype.com/artifact/io.github.hyochan/kmp-iap/$VERSION) - [GitHub (monorepo)](https://github.com/hyodotdev/openiap/tree/main/libraries/kmp-iap) EOF - - name: Create GitHub Release - if: inputs.create_release && success() - uses: softprops/action-gh-release@v3 - with: - tag_name: kmp-iap-${{ steps.version.outputs.VERSION }} - name: kmp-iap ${{ steps.version.outputs.VERSION }} - draft: false - prerelease: ${{ steps.version.outputs.is_prerelease }} - body_path: /tmp/release-notes.md + - name: Build Android release artifacts + if: success() + run: ./gradlew :library:assembleRelease --no-daemon --stacktrace - name: Create release artifacts - if: inputs.create_release && success() + if: success() run: | mkdir -p release-artifacts - cp library/build/outputs/aar/*.aar release-artifacts/ || true - cp library/build/libs/*.jar release-artifacts/ || true + shopt -s nullglob + artifacts=(library/build/outputs/aar/*.aar library/build/libs/*.jar) + if [ ${#artifacts[@]} -eq 0 ]; then + echo "::error::No KMP release artifacts found" + exit 1 + fi + cp "${artifacts[@]}" release-artifacts/ zip -r release-artifacts.zip release-artifacts/ - - name: Upload release artifacts - if: inputs.create_release && success() + - name: Create GitHub Release + if: success() uses: softprops/action-gh-release@v3 with: - files: libraries/kmp-iap/release-artifacts.zip tag_name: kmp-iap-${{ steps.version.outputs.VERSION }} name: kmp-iap ${{ steps.version.outputs.VERSION }} + draft: false + prerelease: ${{ steps.version.outputs.is_prerelease }} + body_path: /tmp/release-notes.md + files: libraries/kmp-iap/release-artifacts.zip diff --git a/.github/workflows/release-maui.yml b/.github/workflows/release-maui.yml index da3f0aac..acb53943 100644 --- a/.github/workflows/release-maui.yml +++ b/.github/workflows/release-maui.yml @@ -18,12 +18,6 @@ on: required: false default: false type: boolean - create_release: - description: "Also create a GitHub Release" - required: false - default: true - type: boolean - concurrency: group: ${{ github.workflow }} cancel-in-progress: false @@ -34,6 +28,9 @@ permissions: env: DOTNET_CLI_TELEMETRY_OPTOUT: "1" DOTNET_NOLOGO: "1" + XCODE_VERSION: 16.4 + XCODEGEN_VERSION: 2.45.4 + XCODEGEN_SHA256: 090ec29491aad50aec10631bf6e62253fed733c50f3aab0f5ffc86bc170bdbef jobs: validate: @@ -59,11 +56,16 @@ jobs: validate-multitarget: name: Validate (net9.0-android / net9.0-ios / net9.0-maccatalyst) - runs-on: macos-latest + runs-on: macos-15 timeout-minutes: 60 steps: - uses: actions/checkout@v6 + - name: Set up Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.XCODE_VERSION }} + - name: Setup JDK 17 uses: actions/setup-java@v5 with: @@ -79,7 +81,7 @@ jobs: run: dotnet workload install maui --skip-sign-check - name: Install xcodegen - run: brew install xcodegen + run: bash scripts/install-xcodegen.sh "$XCODEGEN_VERSION" # Native artifacts must exist before the binding csprojs can compile. - name: Build OpenIAP.xcframework (Apple) @@ -98,13 +100,18 @@ jobs: publish: needs: [validate, validate-multitarget] - runs-on: macos-latest + runs-on: macos-15 timeout-minutes: 60 steps: - uses: actions/checkout@v6 with: fetch-depth: 0 + - name: Set up Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.XCODE_VERSION }} + - name: Setup JDK 17 uses: actions/setup-java@v5 with: @@ -120,7 +127,7 @@ jobs: run: dotnet workload install maui --skip-sign-check - name: Install xcodegen - run: brew install xcodegen + run: bash scripts/install-xcodegen.sh "$XCODEGEN_VERSION" - name: Calculate new version id: version @@ -194,6 +201,14 @@ jobs: echo "✓ Tag maui-iap-$VERSION does not exist, proceeding with release" fi + - name: Checkout release tag (current version) + if: steps.version.outputs.skip_version_commit == 'true' && steps.check_tag.outputs.exists == 'true' + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + RELEASE_TAG="maui-iap-$VERSION" + git checkout "$RELEASE_TAG" + - name: Update package version in OpenIap.Maui.csproj if: steps.version.outputs.skip_version_commit != 'true' env: @@ -289,14 +304,18 @@ jobs: --skip-duplicate - name: Generate release notes - if: inputs.create_release && success() + if: success() id: release_notes env: VERSION: ${{ steps.version.outputs.version }} run: | + RELEASE_REF="HEAD" + if git rev-parse "maui-iap-$VERSION" >/dev/null 2>&1; then + RELEASE_REF="maui-iap-$VERSION" + fi PREV_TAG=$(git for-each-ref --sort=-creatordate --format '%(refname:short)' 'refs/tags/maui-iap-*' | grep -v "$VERSION" | head -n 1) if [ -n "$PREV_TAG" ]; then - CHANGELOG=$(git log "$PREV_TAG..HEAD" --pretty=format:"- %s ([\`%h\`](https://github.com/hyodotdev/openiap/commit/%H))" -- libraries/maui-iap/ packages/google/ packages/apple/ packages/gql/) + CHANGELOG=$(git log "$PREV_TAG..$RELEASE_REF" --pretty=format:"- %s ([\`%h\`](https://github.com/hyodotdev/openiap/commit/%H))" -- libraries/maui-iap/ packages/google/ packages/apple/ packages/gql/) fi if [ -z "$CHANGELOG" ]; then CHANGELOG="- Initial release / no direct code changes — picks up the latest openiap-google / openiap-apple native library updates." @@ -326,9 +345,9 @@ jobs: ### Documentation - - [Setup guide — .NET MAUI](https://www.openiap.dev/docs/setup/maui) - - [API reference](https://www.openiap.dev/docs/apis) - - [Release notes](https://www.openiap.dev/docs/updates/releases) + - [Setup guide — .NET MAUI](https://openiap.dev/docs/setup/maui) + - [API reference](https://openiap.dev/docs/apis) + - [Release notes](https://openiap.dev/docs/updates/releases) ### Links @@ -337,7 +356,7 @@ jobs: EOF - name: Create and push tag - if: inputs.create_release && success() + if: success() env: VERSION: ${{ steps.version.outputs.version }} run: | @@ -351,7 +370,7 @@ jobs: fi - name: Create GitHub Release - if: inputs.create_release && success() + if: success() uses: softprops/action-gh-release@v3 with: tag_name: maui-iap-${{ steps.version.outputs.version }} diff --git a/.github/workflows/release-react-native.yml b/.github/workflows/release-react-native.yml index 6f361078..708dd60b 100644 --- a/.github/workflows/release-react-native.yml +++ b/.github/workflows/release-react-native.yml @@ -19,19 +19,12 @@ on: required: false default: false type: boolean - create_release: - description: "Also create a GitHub Release" - required: false - default: true - type: boolean - concurrency: group: ${{ github.workflow }} cancel-in-progress: false permissions: contents: write - id-token: write jobs: validate-android: @@ -49,7 +42,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: 20.x + node-version: 20 - name: Install Yarn 3 run: corepack enable && corepack prepare yarn@3.6.1 --activate @@ -88,7 +81,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: 20.x + node-version: 20 - name: Install Yarn 3 run: corepack enable && corepack prepare yarn@3.6.1 --activate @@ -134,6 +127,9 @@ jobs: deploy: needs: [validate-android, validate-ios] + permissions: + contents: write + id-token: write runs-on: ubuntu-latest defaults: run: @@ -147,9 +143,20 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: 20.x + node-version: 20 registry-url: "https://registry.npmjs.org" + - name: Checkout release tag (current version) + if: ${{ inputs.version == 'current' }} + run: | + NEW_VERSION=$(node -p "require('./package.json').version") + RELEASE_TAG="react-native-iap-${NEW_VERSION}" + if git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + git checkout "$RELEASE_TAG" + else + echo "Tag $RELEASE_TAG does not exist; current mode will use HEAD." + fi + - name: Disable Corepack run: corepack disable @@ -199,24 +206,66 @@ jobs: echo "is_prerelease=false" >> "$GITHUB_OUTPUT" fi + - name: Check if release tag already exists + id: check_tag + env: + NEW_VERSION: ${{ steps.bump.outputs.version }} + VERSION_TYPE: ${{ inputs.version }} + run: | + RELEASE_TAG="react-native-iap-${NEW_VERSION}" + if git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + if [ "$VERSION_TYPE" = "current" ]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "⚠️ Tag $RELEASE_TAG already exists. Will reuse existing tag (current mode)." + else + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "❌ Tag $RELEASE_TAG already exists. Use 'current' to retry this version." + exit 1 + fi + else + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "✓ Tag $RELEASE_TAG does not exist" + fi + - name: Prepare package (build + codegen) run: node .yarn/releases/yarn-3.6.1.cjs prepare - - name: Commit and tag + - name: Commit version update if: ${{ inputs.version != 'current' }} run: | NEW_VERSION="${{ steps.bump.outputs.version }}" git add package.json git commit -m "chore(release): react-native-iap ${NEW_VERSION}" - git tag -a "react-native-iap-${NEW_VERSION}" -m "Release react-native-iap-${NEW_VERSION}" - name: Push commit and tags if: ${{ inputs.version != 'current' }} + env: + NEW_VERSION: ${{ steps.bump.outputs.version }} run: | - git stash --include-untracked + STASHED=false + if ! git diff --quiet || [ -n "$(git ls-files --others --exclude-standard)" ]; then + git stash push --include-untracked -m "release artifacts" + STASHED=true + fi git pull --rebase origin main - git stash pop || true - git push --follow-tags + if [ "$STASHED" = "true" ]; then + git stash pop + fi + git tag -a "react-native-iap-${NEW_VERSION}" -m "Release react-native-iap-${NEW_VERSION}" + git push origin HEAD:main --follow-tags + + - name: Create tag (current version) + if: ${{ inputs.version == 'current' && steps.check_tag.outputs.exists != 'true' }} + env: + NEW_VERSION: ${{ steps.bump.outputs.version }} + run: | + TAG_TARGET="HEAD" + PUBLISHED_GIT_HEAD=$(npm view "react-native-iap@$NEW_VERSION" gitHead 2>/dev/null || true) + if [ -n "$PUBLISHED_GIT_HEAD" ] && git cat-file -e "$PUBLISHED_GIT_HEAD^{commit}" 2>/dev/null; then + TAG_TARGET="$PUBLISHED_GIT_HEAD" + fi + git tag -a "react-native-iap-${NEW_VERSION}" "$TAG_TARGET" -m "Release react-native-iap-${NEW_VERSION}" + git push origin "react-native-iap-${NEW_VERSION}" - name: Resolve symlinks for npm publish run: | @@ -225,9 +274,27 @@ jobs: fi - name: Ensure npm CLI v11.5.1 or later (required for OIDC) - run: npm install -g npm@latest + run: npm install -g npm@11.5.1 + + - name: Check if npm package already published + id: check_npm + env: + VERSION: ${{ steps.bump.outputs.version }} + run: | + if NPM_OUTPUT=$(npm view "react-native-iap@$VERSION" version 2>&1); then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "⚠️ react-native-iap@$VERSION already exists on npm" + elif echo "$NPM_OUTPUT" | grep -qiE 'E404|404 Not Found'; then + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "✓ react-native-iap@$VERSION does not exist, will publish" + else + echo "❌ Unable to verify react-native-iap@$VERSION on npm" + echo "$NPM_OUTPUT" + exit 1 + fi - name: Publish to npm with OIDC trusted publishing + if: steps.check_npm.outputs.exists == 'false' run: | if [ "${{ steps.bump.outputs.is_prerelease }}" = "true" ]; then npm publish --tag next --access public --provenance @@ -236,14 +303,17 @@ jobs: fi - name: Generate release notes - if: ${{ inputs.create_release }} id: release_notes env: VERSION: ${{ steps.bump.outputs.version }} run: | + RELEASE_REF="HEAD" + if git rev-parse "react-native-iap-$VERSION" >/dev/null 2>&1; then + RELEASE_REF="react-native-iap-$VERSION" + fi PREV_TAG=$(git for-each-ref --sort=-creatordate --format '%(refname:short)' 'refs/tags/react-native-iap-*' | grep -v "$VERSION" | head -n 1) if [ -n "$PREV_TAG" ]; then - CHANGELOG=$(git log "$PREV_TAG..HEAD" --pretty=format:"- %s ([\`%h\`](https://github.com/hyodotdev/openiap/commit/%H))" -- libraries/react-native-iap/ packages/google/ packages/apple/) + CHANGELOG=$(git log "$PREV_TAG..$RELEASE_REF" --pretty=format:"- %s ([\`%h\`](https://github.com/hyodotdev/openiap/commit/%H))" -- libraries/react-native-iap/ packages/google/ packages/apple/) fi if [ -z "$CHANGELOG" ]; then CHANGELOG="- No direct code changes — picks up the latest openiap-google / openiap-apple native library updates. See the consolidated release notes for details." @@ -267,9 +337,9 @@ jobs: ### Documentation - - [Setup guide — React Native](https://www.openiap.dev/docs/setup/react-native) - - [API reference](https://www.openiap.dev/docs/apis) - - [Release notes](https://www.openiap.dev/docs/updates/releases) + - [Setup guide — React Native](https://openiap.dev/docs/setup/react-native) + - [API reference](https://openiap.dev/docs/apis) + - [Release notes](https://openiap.dev/docs/updates/releases) ### Links @@ -278,7 +348,6 @@ jobs: EOF - name: Create GitHub Release - if: ${{ inputs.create_release }} uses: softprops/action-gh-release@v3 with: tag_name: react-native-iap-${{ steps.bump.outputs.version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a905f5ef..ce0d7cdb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,10 @@ on: - major default: patch +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + permissions: contents: write @@ -28,16 +32,18 @@ jobs: - name: Calculate new version id: version + env: + VERSION_TYPE: ${{ github.event.inputs.version }} run: | CURRENT_VERSION=$(jq -r '.spec' openiap-versions.json) echo "Current version: $CURRENT_VERSION" - if [ "${{ github.event.inputs.version }}" = "current" ]; then + if [ "$VERSION_TYPE" = "current" ]; then NEW_VERSION="$CURRENT_VERSION" echo "Using current version (no bump): $NEW_VERSION" else IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" - case "${{ github.event.inputs.version }}" in + case "$VERSION_TYPE" in major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;; minor) MINOR=$((MINOR + 1)); PATCH=0 ;; patch) PATCH=$((PATCH + 1)) ;; @@ -52,10 +58,10 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: latest + bun-version: 1.3.13 - name: Install dependencies - run: bun install + run: bun install --frozen-lockfile - name: Update version in openiap-versions.json if: github.event.inputs.version != 'current' @@ -79,8 +85,30 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add openiap-versions.json packages/*/openiap-versions.json - git commit -m "chore: bump docs to $VERSION" - git pull --rebase origin main || true + git add packages/gql/package.json packages/docs/package.json packages/google/package.json packages/apple/package.json + git commit -m "chore(docs): bump version to $VERSION" + if ! git pull --rebase origin main; then + echo "⚠️ Rebase conflict detected, auto-resolving version files..." + + for conflict_file in $(git diff --name-only --diff-filter=U); do + case "$conflict_file" in + openiap-versions.json|packages/*/openiap-versions.json|packages/gql/package.json|packages/docs/package.json|packages/google/package.json|packages/apple/package.json) + ;; + *) + echo "❌ Unexpected conflict in $conflict_file" + exit 1 + ;; + esac + done + + git show HEAD:openiap-versions.json > /tmp/upstream-openiap-versions.json + jq --arg version "$VERSION" '.spec = $version' /tmp/upstream-openiap-versions.json > openiap-versions.json + ./scripts/sync-versions.sh + git add openiap-versions.json packages/*/openiap-versions.json + git add packages/gql/package.json packages/docs/package.json packages/google/package.json packages/apple/package.json + + GIT_EDITOR=true git rebase --continue || { echo "❌ Rebase continue failed"; exit 1; } + fi git push origin main - name: Create Git tag @@ -90,8 +118,12 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" TAG_NAME="docs-$VERSION" - git tag -a "$TAG_NAME" -m "Release $TAG_NAME" - git push origin "$TAG_NAME" + if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then + echo "Tag $TAG_NAME already exists" + else + git tag -a "$TAG_NAME" -m "Release $TAG_NAME" + git push origin "$TAG_NAME" + fi - name: Create GitHub Release uses: softprops/action-gh-release@v3 diff --git a/.gitignore b/.gitignore index 078132cb..8e1fe1e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Bun node_modules/ +package-lock.json bun.lockb *.lock !libraries/**/yarn.lock @@ -157,4 +158,4 @@ libraries/*/docs/build/ *.ipa *.maui.bundle/ # Resx generated designer code (per-build artifact) -*.resx.designer.cs \ No newline at end of file +*.resx.designer.cs diff --git a/bun.lock b/bun.lock index 5cb4a57c..fb1c85f3 100644 --- a/bun.lock +++ b/bun.lock @@ -11,14 +11,14 @@ }, "packages/apple": { "name": "@hyodotdev/openiap-ios", - "version": "1.2.23", + "version": "2.1.9", "dependencies": { "@hyodotdev/openiap-gql": "workspace:*", }, }, "packages/docs": { "name": "@hyodotdev/openiap-docs", - "version": "2.0.0", + "version": "2.0.2", "dependencies": { "@preact/signals-react": "^3.2.1", "@types/prismjs": "^1.26.5", @@ -32,16 +32,16 @@ "react-toastify": "^11.0.2", }, "devDependencies": { - "@eslint/js": "^9.33.0", + "@eslint/js": "^9.39.4", "@preact/signals-react-transform": "^0.5.2", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@typescript-eslint/eslint-plugin": "^8.39.1", "@typescript-eslint/parser": "^8.39.1", - "@vitejs/plugin-react": "^4.3.1", + "@vitejs/plugin-react": "^5.2.0", "babel-plugin-react-compiler": "^19.1.0-rc.2", "baseline-browser-mapping": "^2.10.22", - "eslint": "^9.21.0", + "eslint": "^9.39.4", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.7", "globals": "^16.3.0", @@ -49,22 +49,21 @@ "lint-staged": "^16.1.5", "lucide-react": "^0.539.0", "prettier": "^3.6.2", - "sharp-cli": "^5.2.0", "typescript": "^5.2.2", "typescript-eslint": "^8.39.1", - "vite": "^5.3.1", + "vite": "^6.4.2", }, }, "packages/google": { "name": "@hyodotdev/openiap-android", - "version": "1.2.12", + "version": "2.1.5", "dependencies": { "@hyodotdev/openiap-gql": "workspace:*", }, }, "packages/gql": { "name": "@hyodotdev/openiap-gql", - "version": "2.0.0", + "version": "2.0.2", "devDependencies": { "@graphql-codegen/add": "^6.0.0", "@graphql-codegen/cli": "^6.0.0", @@ -73,7 +72,7 @@ "handlebars": "^4.7.8", "ts-node": "^10.9.2", "typescript": "^5.9.2", - "vitest": "^4", + "vitest": "^4.1.5", }, }, "packages/kit": { @@ -91,10 +90,10 @@ "@sentry/react": "^10.49.0", "antd": "^6.1.0", "clsx": "^2.1.1", - "convex": "^1.29.2", + "convex": "^1.39.0", "google-auth-library": "^10.6.2", "googleapis": "^157.0.0", - "hono": "^4.9.9", + "hono": "^4.12.18", "hono-openapi": "^1.1.0", "lucide-react": "^0.577.0", "mixpanel-browser": "^2.72.0", @@ -113,7 +112,7 @@ "valibot": "^1.1.0", }, "devDependencies": { - "@eslint/js": "^9.21.0", + "@eslint/js": "^9.39.4", "@playwright/test": "^1.59.1", "@tailwindcss/postcss": "^4.2.4", "@testing-library/jest-dom": "^6", @@ -124,11 +123,11 @@ "@types/node": "^22.13.10", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^5.2.0", "@vitest/ui": "^4", "autoprefixer": "~10", "dotenv": "^16.4.7", - "eslint": "^9.21.0", + "eslint": "^9.39.4", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^15.15.0", @@ -141,7 +140,7 @@ "tailwindcss": "~4", "typescript": "~5.9.3", "typescript-eslint": "^8.24.1", - "vite": "^6.2.0", + "vite": "^6.4.2", "vitest": "^4", }, }, @@ -152,7 +151,7 @@ "openiap-mcp": "./dist/index.js", }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.4", + "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^3.23.8", }, "devDependencies": { @@ -164,6 +163,9 @@ }, "overrides": { "csstype": "3.2.3", + "fast-uri": "3.1.2", + "hono": "4.12.18", + "ip-address": "10.2.0", }, "packages": { "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], @@ -274,57 +276,57 @@ "@envelop/types": ["@envelop/types@5.2.1", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.5.0" } }, "sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], @@ -534,8 +536,6 @@ "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], - "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -798,7 +798,7 @@ "@repeaterjs/repeater": ["@repeaterjs/repeater@3.0.6", "", {}, "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.2", "", { "os": "android", "cpu": "arm" }, "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw=="], @@ -1032,7 +1032,7 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], "@vitest/expect": ["@vitest/expect@4.1.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.5", "@vitest/utils": "4.1.5", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw=="], @@ -1134,8 +1134,6 @@ "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=="], - "bubble-stream-error": ["bubble-stream-error@1.0.0", "", { "dependencies": { "once": "^1.3.3", "sliced": "^1.0.1" } }, "sha512-Rqf0ly5H4HGt+ki/n3m7GxoR2uIGtNqezPlOLX8Vuo13j5/tfPuVvAr84eoGF7sYm6lKdbGnT/3q8qmzuT5Y9w=="], - "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], @@ -1188,14 +1186,10 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], - "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], - "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], @@ -1218,7 +1212,7 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "convex": ["convex@1.37.0", "", { "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.0", "ws": "8.18.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "@clerk/react": "^6.4.3", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "@clerk/react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-xGSx5edIsXCEex3OU2U2N0oyB/cOa9qGwKiImF9yOWqjqZgOkx39idtpdlwNBTBSt4S30oAvs4yeXY5xxPIX3A=="], + "convex": ["convex@1.39.0", "", { "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.0", "ws": "8.18.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "@clerk/react": "^6.4.3", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "@clerk/react", "react"], "bin": { "convex": "bin/main-dev", "convex-bundled": "bin/main.js" } }, "sha512-7ZP/Yi1EL0RUxVDT6knKNThoAoIOinDp+XnWTGTN3+fD3bzdxVYqiBKNbuUo/SvrU80drft11ZLnAv/MjbAHEg=="], "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], @@ -1364,7 +1358,7 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -1422,7 +1416,7 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], @@ -1446,8 +1440,6 @@ "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], - "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], - "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], @@ -1488,8 +1480,6 @@ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], - "glob": ["glob@11.0.3", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA=="], - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], @@ -1540,7 +1530,7 @@ "header-case": ["header-case@2.0.4", "", { "dependencies": { "capital-case": "^1.0.4", "tslib": "^2.0.3" } }, "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q=="], - "hono": ["hono@4.12.16", "", {}, "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg=="], + "hono": ["hono@4.12.18", "", {}, "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ=="], "hono-openapi": ["hono-openapi@1.3.0", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-xDvCWpWEIv0weEmnl3EjRQzqbHIO8LnfzMuYOCmbuyE5aes6aXxLg4vM3ybnoZD5TiTUkA6PuRQPJs3R7WRBig=="], @@ -1586,7 +1576,7 @@ "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], - "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], + "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -1616,8 +1606,6 @@ "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], - "is-directory": ["is-directory@0.3.1", "", {}, "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw=="], - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], @@ -1686,8 +1674,6 @@ "isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="], - "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], @@ -1774,22 +1760,8 @@ "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], - "lodash._baseflatten": ["lodash._baseflatten@3.1.4", "", { "dependencies": { "lodash.isarguments": "^3.0.0", "lodash.isarray": "^3.0.0" } }, "sha512-fESngZd+X4k+GbTxdMutf8ohQa0s3sJEHIcwtu4/LsIQ2JTDzdRxDCMQjW+ezzwRitLmHnacVVmosCbxifefbw=="], - - "lodash._basefor": ["lodash._basefor@3.0.3", "", {}, "sha512-6bc3b8grkpMgDcVJv9JYZAk/mHgcqMljzm7OsbmcE2FGUMmmLQTPHlh/dFqR8LA0GQ7z4K67JSotVKu5058v1A=="], - - "lodash._bindcallback": ["lodash._bindcallback@3.0.1", "", {}, "sha512-2wlI0JRAGX8WEf4Gm1p/mv/SZ+jLijpj0jyaE/AXeuQphzCgD8ZQW4oSpoN8JAopujOFGU3KMuq7qfHBWlGpjQ=="], - - "lodash._pickbyarray": ["lodash._pickbyarray@3.0.2", "", {}, "sha512-tHzBIfgugzI7HV0y8MJS1z/ryWDh8NyD6AV+so9vlplRnhD4qBuwoyDt7g241ad3F43YDFghCN+R3iaFd4Azvw=="], - - "lodash._pickbycallback": ["lodash._pickbycallback@3.0.0", "", { "dependencies": { "lodash._basefor": "^3.0.0", "lodash.keysin": "^3.0.0" } }, "sha512-DVP27YmN0lB+j/Tgd/+gtxfmW/XihgWpQpHptBuwyp2fD9zEBRwwcnw6Qej16LUV8LRFuTqyoc0i6ON97d/C5w=="], - "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], - "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], - - "lodash.isarray": ["lodash.isarray@3.0.4", "", {}, "sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ=="], - "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], @@ -1800,16 +1772,10 @@ "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], - "lodash.keysin": ["lodash.keysin@3.0.8", "", { "dependencies": { "lodash.isarguments": "^3.0.0", "lodash.isarray": "^3.0.0" } }, "sha512-YDB/5xkL3fBKFMDaC+cfGV00pbiJ6XoJIfRmBhv7aR6wWtbCW6IzkiWnTfkiHTF6ALD7ff83dAtB3OEaSoyQPg=="], - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], - "lodash.pick": ["lodash.pick@3.1.0", "", { "dependencies": { "lodash._baseflatten": "^3.0.0", "lodash._bindcallback": "^3.0.0", "lodash._pickbyarray": "^3.0.0", "lodash._pickbycallback": "^3.0.0", "lodash.restparam": "^3.0.0" } }, "sha512-Y04wnFghB7l1dkYINfjdMLpeAGM1IYEjlsGFxvjeewCbVQUlD9jw3M20ThuNrsf6yGmuPLwj60PKP+D6gZ+o2w=="], - - "lodash.restparam": ["lodash.restparam@3.6.1", "", {}, "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw=="], - "lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="], "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], @@ -1958,8 +1924,6 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], - "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], "mixpanel-browser": ["mixpanel-browser@2.78.0", "", { "dependencies": { "@mixpanel/rrweb": "2.0.0-alpha.18.4", "@mixpanel/rrweb-plugin-console-record": "2.0.0-alpha.18.4", "@mixpanel/rrweb-utils": "2.0.0-alpha.18.4", "@types/json-logic-js": "2.0.5", "json-logic-js": "2.0.5" } }, "sha512-K2nsMLnTK0PXcQxhj1aJyGpKyEfo2u7wgZhVm532DTjkoCbJJkuSjDBWJFCH5agEM5oE0aVoCYKd0hZ+i8LsYw=="], @@ -2026,8 +1990,6 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], - "param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="], "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], @@ -2058,8 +2020,6 @@ "path-root-regex": ["path-root-regex@0.1.2", "", {}, "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ=="], - "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], - "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], "path-type": ["path-type@3.0.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg=="], @@ -2090,7 +2050,7 @@ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postcss": ["postcss@8.5.13", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag=="], + "postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], @@ -2152,7 +2112,7 @@ "react-promise-suspense": ["react-promise-suspense@0.3.4", "", { "dependencies": { "fast-deep-equal": "^2.0.1" } }, "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ=="], - "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], "react-router": ["react-router@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw=="], @@ -2256,8 +2216,6 @@ "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], - "sharp-cli": ["sharp-cli@5.2.0", "", { "dependencies": { "bubble-stream-error": "1.0.x", "glob": "11.0.x", "is-directory": "0.3.x", "lodash.pick": "3.1.0", "sharp": "0.34.2", "yargs": "^17.6.2" }, "bin": { "sharp": "bin/cli.js" } }, "sha512-0DQyABFTWkla4FYvjguCQloSgm2htOPfGFur88HXiGu8BPfhb77frWn+E87w8Oog0eSHF4/uMt5AFhpvfY/Zrg=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], @@ -2276,16 +2234,12 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], - "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], - "sliced": ["sliced@1.0.1", "", {}, "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA=="], - "snake-case": ["snake-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg=="], "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], @@ -2476,7 +2430,7 @@ "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], - "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + "vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="], "vitest": ["vitest@4.1.5", "", { "dependencies": { "@vitest/expect": "4.1.5", "@vitest/mocker": "4.1.5", "@vitest/pretty-format": "4.1.5", "@vitest/runner": "4.1.5", "@vitest/snapshot": "4.1.5", "@vitest/spy": "4.1.5", "@vitest/utils": "4.1.5", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.5", "@vitest/browser-preview": "4.1.5", "@vitest/browser-webdriverio": "4.1.5", "@vitest/coverage-istanbul": "4.1.5", "@vitest/coverage-v8": "4.1.5", "@vitest/ui": "4.1.5", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg=="], @@ -2576,12 +2530,12 @@ "@hyodotdev/openiap-kit/react-router-dom": ["react-router-dom@7.14.2", "", { "dependencies": { "react-router": "7.14.2" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ=="], - "@hyodotdev/openiap-kit/vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="], - "@hyodotdev/openiap-mcp-server/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], "@inquirer/core/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=="], + "@mixpanel/rrweb-snapshot/postcss": ["postcss@8.5.13", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag=="], + "@modelcontextprotocol/sdk/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "@node-rs/argon2-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@0.45.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w=="], @@ -2604,6 +2558,8 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@tailwindcss/postcss/postcss": ["postcss@8.5.13", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag=="], + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], @@ -2638,10 +2594,6 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "color/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "convex/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="], "dir-glob/path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], @@ -2658,8 +2610,6 @@ "gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "graphql-config/cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], @@ -2700,10 +2650,6 @@ "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], - "sharp-cli/sharp": ["sharp@0.34.2", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.4", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.2", "@img/sharp-darwin-x64": "0.34.2", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", "@img/sharp-libvips-linux-arm64": "1.1.0", "@img/sharp-libvips-linux-ppc64": "1.1.0", "@img/sharp-libvips-linux-s390x": "1.1.0", "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", "@img/sharp-linux-arm": "0.34.2", "@img/sharp-linux-arm64": "0.34.2", "@img/sharp-linux-s390x": "0.34.2", "@img/sharp-linux-x64": "0.34.2", "@img/sharp-linuxmusl-arm64": "0.34.2", "@img/sharp-linuxmusl-x64": "0.34.2", "@img/sharp-wasm32": "0.34.2", "@img/sharp-win32-arm64": "0.34.2", "@img/sharp-win32-ia32": "0.34.2", "@img/sharp-win32-x64": "0.34.2" } }, "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg=="], - - "simple-swizzle/is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], - "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], @@ -2716,7 +2662,7 @@ "sync-fetch/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], - "vitest/vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="], + "vite/postcss": ["postcss@8.5.13", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag=="], "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], @@ -2730,8 +2676,6 @@ "@hyodotdev/openiap-kit/react-router-dom/react-router": ["react-router@7.14.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw=="], - "@hyodotdev/openiap-kit/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "@hyodotdev/openiap-mcp-server/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -2768,8 +2712,6 @@ "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "color/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "convex/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="], "convex/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="], @@ -2824,8 +2766,6 @@ "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "glob/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], - "graphql-config/cosmiconfig/path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], "graphql-config/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], @@ -2850,100 +2790,10 @@ "npm-run-all/cross-spawn/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], - "sharp-cli/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.1.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg=="], - - "sharp-cli/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.1.0" }, "os": "darwin", "cpu": "x64" }, "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g=="], - - "sharp-cli/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA=="], - - "sharp-cli/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ=="], - - "sharp-cli/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.1.0", "", { "os": "linux", "cpu": "arm" }, "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA=="], - - "sharp-cli/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew=="], - - "sharp-cli/sharp/@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.1.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ=="], - - "sharp-cli/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.1.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA=="], - - "sharp-cli/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q=="], - - "sharp-cli/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w=="], - - "sharp-cli/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A=="], - - "sharp-cli/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.1.0" }, "os": "linux", "cpu": "arm" }, "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ=="], - - "sharp-cli/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q=="], - - "sharp-cli/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.1.0" }, "os": "linux", "cpu": "s390x" }, "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw=="], - - "sharp-cli/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ=="], - - "sharp-cli/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA=="], - - "sharp-cli/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA=="], - - "sharp-cli/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.2", "", { "dependencies": { "@emnapi/runtime": "^1.4.3" }, "cpu": "none" }, "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ=="], - - "sharp-cli/sharp/@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ=="], - - "sharp-cli/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw=="], - - "sharp-cli/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw=="], - - "vitest/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "@fastify/otel/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "@hyodotdev/openiap-kit/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "@inquirer/core/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], @@ -2952,60 +2802,12 @@ "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "graphql-config/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "npm-run-all/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], "npm-run-all/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], - "vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "@inquirer/core/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], diff --git a/knowledge/_claude-context/context.md b/knowledge/_claude-context/context.md index 41204e12..9b62d064 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-08T10:48:46.765Z +> Last updated: 2026-05-16T12:59:43.317Z > > Usage: `claude --context knowledge/_claude-context/context.md` @@ -280,7 +280,8 @@ openiap/ │ ├── expo-iap/ # Expo (npm, Bun, Expo Modules) │ ├── flutter_inapp_purchase/ # Flutter (pub.dev, Dart) │ ├── godot-iap/ # Godot 4.x (GitHub Release, GDScript) -│ └── kmp-iap/ # Kotlin Multiplatform (Maven Central) +│ ├── kmp-iap/ # Kotlin Multiplatform (Maven Central) +│ └── maui-iap/ # .NET MAUI / C# (NuGet) ├── knowledge/ # Shared knowledge base (SSOT) │ ├── internal/ # Project philosophy (HIGHEST PRIORITY) │ ├── external/ # External API reference @@ -299,7 +300,7 @@ Libraries reference local `packages/apple` and `packages/google` source directly **Purpose:** Single source of truth for type definitions. - Contains GraphQL schema defining all OpenIAP types -- Generates types for: TypeScript, Swift, Kotlin, Dart +- Generates types for: TypeScript, Swift, Kotlin, Dart, GDScript, C# - **RULE:** `Types.swift` / `Types.kt` are AUTO-GENERATED. Never edit directly. ```bash @@ -308,16 +309,20 @@ cd packages/gql && bun run generate ``` Generated files: + - TypeScript: `src/generated/types.ts` -- Swift: `dist/swift/Types.swift` -- Kotlin: `dist/kotlin/Types.kt` -- Dart: `dist/dart/types.dart` +- Swift: `src/generated/Types.swift` +- Kotlin: `src/generated/Types.kt` +- Dart: `src/generated/types.dart` +- GDScript: `src/generated/types.gd` +- C#: `src/generated/Types.cs` ### packages/apple **Purpose:** iOS/macOS StoreKit 2 implementation. Directory structure: + ``` Sources/ ├── Models/ # Official OpenIAP types (matches openiap.dev/docs/types) @@ -338,13 +343,14 @@ Sources/ **Purpose:** Android Google Play Billing implementation. Directory structure: + ``` openiap/src/main/ ├── java/dev/hyo/openiap/ │ ├── OpenIapModule.kt │ ├── Models.kt +│ ├── Types.kt # AUTO-GENERATED - DO NOT EDIT │ └── utils/ # Internal helpers -└── Types.kt # AUTO-GENERATED - DO NOT EDIT ``` ### packages/docs @@ -465,6 +471,7 @@ fun fetchProducts(productIds: List, callback: (List) -> **CRITICAL**: All async/Promise-returning operations in the GraphQL schema MUST include `# Future` comment above the field definition. The `# Future` comment tells the type generator to wrap the return type appropriately: + - TypeScript: `Promise` - Swift: `async` - Kotlin: `suspend` @@ -768,7 +775,7 @@ Version is managed in `openiap-versions.json`: **To update GQL types:** -1. Edit `openiap-versions.json` - change `"gql"` version +1. Edit `openiap-versions.json` - change the `"spec"` version 2. Run `./scripts/generate-types.sh` 3. Run `swift test` to verify compatibility @@ -886,13 +893,13 @@ GraphQL schema ─► generated types ─► public API ─► native bridge ─ For every new/changed handler in the generated types, verify **all five** of these per target library before considering the change shippable: -| Library | 1. Type declared | 2. Public API exposed | 3. Platform bridge | 4. Wired into handlers bundle | 5. Test coverage | -| -------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **react-native-iap** | `src/types.ts` (generated) | `src/index.ts` export (Nitro or composed TS) | `ios/HybridRnIap.swift` (iOS), `android/.../HybridRnIap.kt` (Android) | Not required (flat exports) | Mock stub in all 4 `mockIap` objects in `__tests__/` (per memory) | -| **expo-iap** | `src/types.ts` (generated) | `src/modules/ios.ts` / `android.ts` export, re-exported from `src/index.ts` | `ios/ExpoIapModule.swift` `AsyncFunction`, `android/.../ExpoIapModule.kt` | Not required (flat exports) | `src/modules/__tests__/*.test.ts` | -| **flutter_inapp_purchase** | `lib/types.dart` (generated) | getter on `FlutterInappPurchase` in `lib/flutter_inapp_purchase.dart` | `case "":` in `ios/Classes/FlutterInappPurchasePlugin.swift`, Android plugin `onMethodCall` | `queryHandlers` / `mutationHandlers` / `subscriptionHandlers` bundles near the bottom of `flutter_inapp_purchase.dart` | Mock + test in `test/ios_methods_test.dart` (and the `errors_unit_test.dart` error-mapping test) | -| **kmp-iap** | `library/src/commonMain/.../openiap/Types.kt` (generated interface) | exposed via `KmpInAppPurchase` / `kmpIapInstance` | `library/src/iosMain/.../InAppPurchaseIOS.kt` — must call `openIapModule.WithCompletion { ... }`, **never** `throw UnsupportedOperationException` | Not required (interface dispatch) | `library/src/commonTest/` if testable cross-platform | -| **godot-iap** | `addons/godot-iap/types.gd` (generated) | public `snake_case` function in `addons/godot-iap/godot_iap.gd` | `ios-gdextension/Sources/GodotIap/GodotIap.swift` (iOS), `android/src/main/java/.../GodotIap.java` (Android) | Not required | Manual testing — no automated test suite yet | +| Library | 1. Type declared | 2. Public API exposed | 3. Platform bridge | 4. Wired into handlers bundle | 5. Test coverage | +| -------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **react-native-iap** | `src/types.ts` (generated) | `src/index.ts` export (Nitro or composed TS) | `ios/HybridRnIap.swift` (iOS), `android/.../HybridRnIap.kt` (Android) | Not required (flat exports) | Mock stub in all 4 `mockIap` objects in `__tests__/` (per memory) | +| **expo-iap** | `src/types.ts` (generated) | `src/modules/ios.ts` / `android.ts` export, re-exported from `src/index.ts` | `ios/ExpoIapModule.swift` `AsyncFunction`, `android/.../ExpoIapModule.kt` | Not required (flat exports) | `src/modules/__tests__/*.test.ts` | +| **flutter_inapp_purchase** | `lib/types.dart` (generated) | getter on `FlutterInappPurchase` in `lib/flutter_inapp_purchase.dart` | `case "":` in `ios/Classes/FlutterInappPurchasePlugin.swift`, Android plugin `onMethodCall` | `queryHandlers` / `mutationHandlers` / `subscriptionHandlers` bundles near the bottom of `flutter_inapp_purchase.dart` | Mock + test in `test/ios_methods_test.dart` (and the `errors_unit_test.dart` error-mapping test) | +| **kmp-iap** | `library/src/commonMain/.../openiap/Types.kt` (generated interface) | exposed via `KmpInAppPurchase` / `kmpIapInstance` | `library/src/iosMain/.../InAppPurchaseIOS.kt` — must call `openIapModule.WithCompletion { ... }`, **never** `throw UnsupportedOperationException` | Not required (interface dispatch) | `library/src/commonTest/` if testable cross-platform | +| **godot-iap** | `addons/godot-iap/types.gd` (generated) | public `snake_case` function in `addons/godot-iap/godot_iap.gd` | `ios-gdextension/Sources/GodotIap/GodotIap.swift` (iOS), `android/src/main/java/.../GodotIap.java` (Android) | Not required | Manual testing — no automated test suite yet | | **maui-iap** | `src/OpenIap.Maui/Types.cs` (generated) | `OpenIap.QueryResolver` / `MutationResolver` interfaces in `Types.cs`; `IOpenIap` adds the listener-stream contract; static facade is `OpenIap.Maui.Iap`; IAPKit helpers mirror TypeScript via `Iap.KitApi(...)`, `Iap.ConnectWebhookStream(...)`, `Iap.ParseWebhookEventData(...)`, and `Iap.WebhookEventTypes` | Android: `OpenIapMauiModule.kt` in `libraries/maui-iap/android/openiap/` (JSON-shaped Java facade over `packages/google`), bound by `OpenIap.Maui.Bindings.Android.csproj`, consumed by `Platforms/Android/OpenIapAndroid.cs`. iOS / macCatalyst: existing `OpenIapModule+ObjC.swift` bridge in `packages/apple`, bound by hand-written `OpenIap.Maui.Bindings.iOS/ApiDefinition.cs`, consumed by `Platforms/iOS/OpenIapIOS.cs` (+ subclass `OpenIapMacCatalyst`). | Not required (interface dispatch) | Example app `libraries/maui-iap/example/OpenIap.Maui.Example` builds for net9.0-android / net9.0-ios / net9.0-maccatalyst (manual device testing for purchase flow); no xUnit tests yet | ### Platform suffix rule (who needs what) @@ -981,7 +988,7 @@ The Google package supports **two build flavors**: ### Critical Rules -1. **DO NOT edit generated files**: `openiap/src/main/Types.kt` is auto-generated +1. **DO NOT edit generated files**: `openiap/src/main/java/dev/hyo/openiap/Types.kt` is auto-generated 2. Put reusable Kotlin helpers in `openiap/src/main/java/dev/hyo/openiap/utils/` 3. Run `./scripts/generate-types.sh` to regenerate types 4. **Test BOTH flavors** when making changes to shared code @@ -1038,7 +1045,7 @@ Meta Horizon has different APIs from Google Play: ### Updating openiap-gql Version -1. Edit `openiap-versions.json` and update the `gql` field +1. Edit `openiap-versions.json` and update the `spec` field 2. Run `./scripts/generate-types.sh` to download and regenerate Types.kt 3. Compile BOTH flavors to verify: ```bash @@ -1158,7 +1165,8 @@ packages/gql/codegen/ │ ├── swift.ts # Swift plugin (Codable, ErrorCode handling) │ ├── kotlin.ts # Kotlin plugin (sealed interface, fromJson/toJson) │ ├── dart.ts # Dart plugin (sealed class, factory constructors) -│ └── gdscript.ts # GDScript plugin (Godot engine) +│ ├── gdscript.ts # GDScript plugin (Godot engine) +│ └── csharp.ts # C# plugin (.NET MAUI) └── templates/ # Handlebars templates (optional) ``` @@ -1185,6 +1193,7 @@ Each plugin handles language-specific requirements: | **Kotlin** | sealed interface, fromJson/toJson with nullable patterns | | **Dart** | extends/implements, factory constructors, sealed class | | **GDScript** | \_init(), from_json/to_json, Variant type | +| **C#** | records, JsonConverter, [JsonPolymorphic] unions | ### Scripts @@ -1195,6 +1204,7 @@ Each plugin handles language-specific requirements: | `generate:kotlin` | Generate Kotlin types (IR-based plugin) | | `generate:dart` | Generate Dart types (IR-based plugin) | | `generate:gdscript` | Generate GDScript types (IR-based plugin) | +| `generate:csharp` | Generate C# / MAUI types (IR-based plugin) | | `generate` | Generate all types + sync to platforms | | `sync` | Sync generated types to platform packages | @@ -1211,6 +1221,7 @@ bun run generate:swift bun run generate:kotlin bun run generate:dart bun run generate:gdscript +bun run generate:csharp ``` ### Generated Files @@ -1222,6 +1233,7 @@ bun run generate:gdscript | `src/generated/Types.kt` | Android | Data classes & sealed interfaces | | `src/generated/types.dart` | Flutter | Classes & sealed classes | | `src/generated/types.gd` | Godot | GDScript classes | +| `src/generated/Types.cs` | .NET MAUI | C# records & JSON converters | ### Adding a New Language @@ -1533,10 +1545,22 @@ Before adding or editing a `Package Releases` list: 2. Read the current package metadata from `origin/main`, not from memory. 3. For planned patch releases, add exactly one patch version to each affected framework package and label the block `Planned Package Releases`. -4. For published release links, confirm each tag exists with - `gh release view --repo hyodotdev/openiap` before adding an ``. -5. If a release workflow is still running, keep the entry as plain text with - planned wording. Add links only after the GitHub Release exists. +4. If the user explicitly asks to write the note as already released, says to + "assume it will be deployed/published", or asks to follow the existing linked + release-note style, do **not** use `Planned Package Releases` or + `(planned)`. Write the block as `Package Releases`, add the expected GitHub + Release tag link (for example `godot-iap-2.2.8`), and use shipped wording + such as "Publishes" / "Ships" instead of "Prepares". +5. For links to releases that should already exist in GitHub, confirm each tag + exists with `gh release view --repo hyodotdev/openiap` before adding an + ``. This existence check is skipped only when step 4 applies because + the user explicitly requested an assumed post-release note. +6. If a release workflow is still running and the user has not requested an + already-released note, keep the entry as plain text with planned wording. Add + links only after the GitHub Release exists. +7. Run `bun run audit:docs`; the audit fails when a published + `Package Releases` block contains a package/version item without a GitHub + Release link. Do not use `openiap-versions.json` to derive React Native, Expo, Flutter, Godot, KMP, or MAUI versions; that manifest tracks only `spec`, `google`, and @@ -1637,15 +1661,15 @@ Fix purchase validation error 1. Updates `openiap-versions.json` 2. Commits version change to main -3. Creates Git tag `apple-v1.2.24` +3. Creates Git tag `` (bare semver) 4. Builds and tests Swift package 5. Validates and publishes to CocoaPods 6. Creates GitHub Release **Result:** -- CocoaPods: `pod 'openiap', '~> 1.2.24'` -- Swift Package Manager: `.package(url: "https://github.com/hyodotdev/openiap.git", from: "1.2.24")` +- CocoaPods: `pod 'openiap', '~> '` +- Swift Package Manager: `.package(url: "https://github.com/hyodotdev/openiap.git", from: "")` ### Deploying Google Package (Android) @@ -1653,27 +1677,27 @@ Fix purchase validation error 1. Go to Actions -> "Google Release" 2. Click "Run workflow" -3. Enter version (e.g., `1.2.14`) +3. Enter version (e.g., ``) 4. Click "Run workflow" **What happens:** 1. Updates `openiap-versions.json` 2. Commits version change to main -3. Creates Git tag `google-v1.2.14` +3. Creates Git tag `google-` 4. Builds and tests Android library 5. Publishes to Maven Central 6. Creates GitHub Release with artifacts (AAR, JAR) **Result:** -- Maven Central: `implementation("io.github.hyochan.openiap:openiap-google:1.2.14")` +- Maven Central: `implementation("io.github.hyochan.openiap:openiap-google:")` ### Deploying Documentation ```bash # From monorepo root -npm run deploy 1.2.0 +npm run deploy ``` This will: @@ -1682,9 +1706,17 @@ This will: 2. Trigger GitHub Actions workflow to: - Regenerate types for all platforms - Create release artifacts (TypeScript, Dart, Kotlin, Swift) - - Create Git tag `v1.2.0` + - Create Git tag `docs-` - Create GitHub Release with artifacts +`npm run deploy` uses the current `spec` value from +`openiap-versions.json`. To deploy a different spec version, pass it +explicitly: + +```bash +npm run deploy 1.2.0 +``` + --- ## Release Tag Conventions @@ -1751,7 +1783,8 @@ This file is automatically managed by CI/CD workflows during releases: - Apple releases update `apple` version - Google releases update `google` version - GQL releases update `spec` version -- Deploy script (`npm run deploy`) updates `spec` version +- Deploy script (`npm run deploy`) uses the current `spec` version by default, + and updates `spec` only when an explicit version is passed The manifest is only for the shared spec and native platform packages: `spec`, `google`, and `apple`. Framework library package versions @@ -1766,7 +1799,7 @@ Manual edits will cause version conflicts and deployment issues. Always use the **Rule:** Feature PRs must NEVER touch version fields in `openiap-versions.json`. Version bumps happen only via: 1. Release workflows (Apple Release, Google Release) -2. Deploy script (`npm run deploy `) +2. Deploy script (`npm run deploy`, optionally `npm run deploy `) 3. CI auto-bump after merge @@ -1918,6 +1951,16 @@ wrapper tabs use the suffixed name (`syncIOS()`, etc.) — except in `packages/google` Kotlin (the Android-only native), where convention strips the `Android` suffix from method names. +### R9 — Published package release lists use links + +When a release-note block is labeled `Package Releases`, every package/version +item in that list must link to the corresponding GitHub Release. Use +`Planned Package Releases` only while the release workflow is still running or +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. + ## Pre-commit checklist Run before every `git push` on docs / SDK changes: @@ -1930,7 +1973,7 @@ bun run lint # 2. Cross-library typecheck for SDKs you touched cd libraries/expo-iap && bun run lint:tsc -cd libraries/react-native-iap && yarn typecheck # ignore example-expo errors +cd libraries/react-native-iap && yarn typecheck cd libraries/flutter_inapp_purchase && dart analyze lib cd packages/apple && swift build cd packages/google && ./gradlew :openiap:compilePlayDebugKotlin @@ -1959,13 +2002,256 @@ file, line, and the offending mention. Run with: ```bash -cd /Users/hyo/Github/hyodotdev/openiap +cd bun run scripts/audit-docs.ts ``` Exit code 1 means at least one drift; 0 means clean. +--- + + + +# GV Cloud Workspace Policy + +> **Priority: MANDATORY** +> Follow this policy when using TabTabTab `gv` cloud environments with OpenIAP. + +`gv` can be useful for OpenIAP as a safe remote maintenance runner, not as a +release, signing, or production-credential environment. Treat every GV +workspace as an external cloud workspace with GitHub access and no local secret +trust by default. + +## Safe role for OpenIAP + +Use GV for secret-free OSS maintenance work: + +- Documentation edits, release notes, docs typecheck, and docs consistency + audits. +- `packages/gql` tests and schema/codegen review work that does not require + private credentials. +- `packages/kit` typecheck and unit tests that run without production env vars. +- PR review response work on isolated branches/worktrees. +- Long-running lint/test/build smoke checks that should survive local laptop + sleep or high local resource use. + +Do not treat GV as the source of truth for full OpenIAP release validation. +Native Apple signing, Play/App Store production credentials, package publishing, +and deployment stay in the existing local or CI release systems. + +## Required boundaries + +Always keep these boundaries unless the repository owner explicitly changes this +policy: + +- Onboard the repo with env capture disabled: + + ```bash + gv repo add . --skip-env + ``` + +- First test of any new GV version or environment should be: + + ```bash + gv repo add . --dry-run --skip-env + ``` + +- GitHub App access must be limited to the selected `hyodotdev/openiap` + repository. Do not grant all-repository access. +- Do not enable OpenAI/Codex auth mirroring for OpenIAP by default. +- Do not enable local profile, CLI, shell, editor, or credential mirroring by + default. +- Do not add production, payment, signing, release, or deployment secrets to GV. +- If credentials are ever needed for a GV experiment, use sandbox/test-only + credentials with explicit owner approval. + +## Forbidden commands and actions + +Never run or recommend these for OpenIAP GV work: + +```bash +gv repo add . --yes +gv repo env list --reveal +gv env info --reveal +gv env info --qr +``` + +Also do not upload, reveal, or sync: + +- `.env`, `.env.local`, `.env.*` +- App Store Connect `.p8` keys +- Google service-account JSON files +- signing keys, provisioning profiles, certificates, keystores, and JKS files +- npm, NuGet, Maven Central, CocoaPods, Fly, Convex, App Store, Google Play, or + payment provider credentials + +One-time GV login URLs and workspace URLs should be treated as sensitive access +links. Do not paste them into issues, PRs, public docs, or long-lived logs. + +## Known GV baseline for this repo + +Validated on 2026-05-08 with a GV `agent-sandbox` environment: + +- Repo onboarding with `--skip-env` completed. +- `gv repo env list --repo openiap --json` returned an empty env var list. +- OpenAI auth status was disabled. +- GitHub access was enabled only after selected-repository approval. +- Cloud clone was clean on `main` from + `https://github.com/hyodotdev/openiap.git`. +- The default environment had `node`, `npm`, `corepack`, `python3`, `git`, and + `docker`. +- The default environment did not have `bun`, `yarn`, `java`, `swift`, + `flutter`, or `dotnet`. +- No `.devcontainer/devcontainer.json` existed in the repo at validation time. + +Because Bun is not available in the default GV environment, the safe current +pattern is to run Bun checks inside Docker containers with the workspace mounted +read-only. + +## Day-to-day usage + +Use GV by opening an agent or editor attached to the cloud environment, then +give the task prompt there. The prompt is not a shell command. + +```bash +gv env use agent-sandbox + +# Open a cloud-attached agent/editor. +gv open opencode --env agent-sandbox +gv open codex --env agent-sandbox +``` + +Use `gv ssh` for direct terminal checks in the cloud workspace: + +```bash +gv ssh --env agent-sandbox +cd ~/workspace/openiap +git status --short --branch +``` + +For investigation-only work, make the boundary explicit: + +```text +Investigate issue 104 and the GQL -> SDK sync flow. +List the affected packages and propose a fix plan. +Do not change code, commit, push, create PRs, read env files, or run deploy, +release, signing, publish, or credential-related commands. +``` + +For maintenance work that may edit code, require an isolated branch and scoped +verification: + +```text +Create a branch named codex/. +Make the smallest safe change for the requested docs/GQL/kit issue. +Do not touch env, signing, release, deploy, or publish files. +Run only the relevant secret-free checks, then summarize the diff and results. +``` + +## Safe verification pattern + +Prefer an ephemeral Docker container with a read-only repo mount and an internal +copy: + +```bash +gv ssh --env agent-sandbox -- \ + 'set -eu + OPENIAP_PATH="${OPENIAP_PATH:-$HOME/workspace/openiap}" + test -d "$OPENIAP_PATH" + docker run --rm \ + -v "$OPENIAP_PATH:/src:ro" \ + -w /work \ + oven/bun:1.3.13 \ + bash -lc "cp -a /src/. /work && bun install --frozen-lockfile && bun run audit:docs"' +``` + +Why this pattern: + +- `:ro` prevents the container from writing to the GV checkout. +- `/work` is a temporary container copy, so `node_modules`, build output, and + generated files disappear when the container exits. +- It avoids syncing local env files or local uncommitted changes. + +After any GV run, verify both workspace cleanliness and env state: + +```bash +gv ssh --env agent-sandbox -- \ + 'cd ~/workspace/openiap && git status --short --branch' + +gv repo env list --repo openiap --json +``` + +## Verified safe smoke checks + +These checks have run successfully inside the Docker `/work` copy in the +GV read-only pattern, not directly in the default GV host shell. Bun is not +available in the default GV environment unless a future setup script installs +it. + +```bash +# GQL tests +cd packages/gql && bun run test + +# Docs typecheck +cd packages/docs && bun run typecheck + +# Kit typecheck and tests +cd packages/kit && bun run typecheck && bun run test + +# Docs consistency audit +bun run audit:docs +``` + +Use these as the first GV regression suite for docs, GQL, and IAPKit +maintenance work. + +## Out of scope for GV until explicitly proven + +Do not use GV as the default runner for: + +- `packages/apple` SwiftPM/Xcode signing or release workflows. +- iOS/macOS Godot, Expo, React Native, KMP, Flutter, or MAUI device builds. +- Android/KMP release publishing that needs Maven Central signing credentials. +- Flutter pub.dev, npm, NuGet, CocoaPods trunk, GitHub release, or deployment + publishing. +- Fly/Convex production deploys. +- Any flow that requires production IAP, payment, App Store Connect, Google + Play, or signing credentials. + +Linux-friendly Android/KMP checks may become reasonable after the repository has +a minimal GV/devcontainer setup with Java installed, but production credentials +still remain out of scope. + +## Branch and PR workflow + +Use GV for isolated work, not direct `main` edits: + +1. Start from the clean cloud clone. +2. Create a branch such as `codex/docs-gv-audit` or `codex/kit-gv-smoke`. +3. Run only secret-free checks. +4. Review `git diff` and `git status`. +5. Push only intentional source changes. +6. Open a PR for normal CI review. + +Do not push release, signing, or deployment changes from GV without explicit +owner approval. + +## Future improvement + +If GV becomes part of regular maintenance, add a minimal devcontainer or setup +script for the Linux-friendly subset: + +- Bun pinned to the root `packageManager`. +- Node/Corepack. +- Java for Gradle checks. +- Optional Android command-line tooling if needed. + +Do not add Swift, Xcode, Flutter, .NET, signing tools, or production secret +setup to the first GV devcontainer. Keep the first iteration small and focused +on docs, GQL, kit, and non-release Android/KMP smoke checks. + + --- @@ -2513,7 +2799,7 @@ Google Play Billing Library enables in-app purchases and subscriptions on Androi | 8.2.1 | 2025-12-15 | Bug fix for `isBillingProgramAvailableAsync()` and `createBillingProgramReportingDetailsAsync()` | | 8.3 | 2025-12-23 | External Payments program (Japan only), developer billing options | -**Current Version**: 8.3.0 (as of January 2026) +**Current Version**: 8.3.0 (as of April 2026) ## Core Classes diff --git a/knowledge/external/google-billing-api.md b/knowledge/external/google-billing-api.md index 8cd16955..e5a9b386 100644 --- a/knowledge/external/google-billing-api.md +++ b/knowledge/external/google-billing-api.md @@ -17,7 +17,7 @@ Google Play Billing Library enables in-app purchases and subscriptions on Androi | 8.2.1 | 2025-12-15 | Bug fix for `isBillingProgramAvailableAsync()` and `createBillingProgramReportingDetailsAsync()` | | 8.3 | 2025-12-23 | External Payments program (Japan only), developer billing options | -**Current Version**: 8.3.0 (as of January 2026) +**Current Version**: 8.3.0 (as of April 2026) ## Core Classes diff --git a/knowledge/internal/04-platform-packages.md b/knowledge/internal/04-platform-packages.md index 6a4344eb..3193c983 100644 --- a/knowledge/internal/04-platform-packages.md +++ b/knowledge/internal/04-platform-packages.md @@ -37,7 +37,7 @@ Version is managed in `openiap-versions.json`: **To update GQL types:** -1. Edit `openiap-versions.json` - change `"gql"` version +1. Edit `openiap-versions.json` - change the `"spec"` version 2. Run `./scripts/generate-types.sh` 3. Run `swift test` to verify compatibility @@ -307,7 +307,7 @@ Meta Horizon has different APIs from Google Play: ### Updating openiap-gql Version -1. Edit `openiap-versions.json` and update the `gql` field +1. Edit `openiap-versions.json` and update the `spec` field 2. Run `./scripts/generate-types.sh` to download and regenerate Types.kt 3. Compile BOTH flavors to verify: ```bash diff --git a/knowledge/internal/06-git-deployment.md b/knowledge/internal/06-git-deployment.md index 7f96ffbf..d10ea6b0 100644 --- a/knowledge/internal/06-git-deployment.md +++ b/knowledge/internal/06-git-deployment.md @@ -88,15 +88,15 @@ Fix purchase validation error 1. Updates `openiap-versions.json` 2. Commits version change to main -3. Creates Git tag `apple-v1.2.24` +3. Creates Git tag `` (bare semver) 4. Builds and tests Swift package 5. Validates and publishes to CocoaPods 6. Creates GitHub Release **Result:** -- CocoaPods: `pod 'openiap', '~> 1.2.24'` -- Swift Package Manager: `.package(url: "https://github.com/hyodotdev/openiap.git", from: "1.2.24")` +- CocoaPods: `pod 'openiap', '~> '` +- Swift Package Manager: `.package(url: "https://github.com/hyodotdev/openiap.git", from: "")` ### Deploying Google Package (Android) @@ -104,21 +104,21 @@ Fix purchase validation error 1. Go to Actions -> "Google Release" 2. Click "Run workflow" -3. Enter version (e.g., `1.2.14`) +3. Enter version (e.g., ``) 4. Click "Run workflow" **What happens:** 1. Updates `openiap-versions.json` 2. Commits version change to main -3. Creates Git tag `google-v1.2.14` +3. Creates Git tag `google-` 4. Builds and tests Android library 5. Publishes to Maven Central 6. Creates GitHub Release with artifacts (AAR, JAR) **Result:** -- Maven Central: `implementation("io.github.hyochan.openiap:openiap-google:1.2.14")` +- Maven Central: `implementation("io.github.hyochan.openiap:openiap-google:")` ### Deploying Documentation @@ -133,7 +133,7 @@ This will: 2. Trigger GitHub Actions workflow to: - Regenerate types for all platforms - Create release artifacts (TypeScript, Dart, Kotlin, Swift) - - Create Git tag `v` + - Create Git tag `docs-` - Create GitHub Release with artifacts `npm run deploy` uses the current `spec` value from diff --git a/knowledge/internal/07-docs-consistency.md b/knowledge/internal/07-docs-consistency.md index 986e0336..fef346e7 100644 --- a/knowledge/internal/07-docs-consistency.md +++ b/knowledge/internal/07-docs-consistency.md @@ -164,7 +164,7 @@ bun run lint # 2. Cross-library typecheck for SDKs you touched cd libraries/expo-iap && bun run lint:tsc -cd libraries/react-native-iap && yarn typecheck # ignore example-expo errors +cd libraries/react-native-iap && yarn typecheck cd libraries/flutter_inapp_purchase && dart analyze lib cd packages/apple && swift build cd packages/google && ./gradlew :openiap:compilePlayDebugKotlin @@ -193,7 +193,7 @@ file, line, and the offending mention. Run with: ```bash -cd /Users/hyo/Github/hyodotdev/openiap +cd bun run scripts/audit-docs.ts ``` diff --git a/libraries/expo-iap/CLAUDE.md b/libraries/expo-iap/CLAUDE.md index 80bf9704..00959cb1 100644 --- a/libraries/expo-iap/CLAUDE.md +++ b/libraries/expo-iap/CLAUDE.md @@ -79,11 +79,11 @@ Before committing any changes: - **ID fields**: Use `Id` instead of `ID` (e.g., `productId`, `transactionId`, not `productID`, `transactionID`) - **Consistent naming**: This applies to functions, types, and file names -- **Deprecation**: Fields without platform suffixes will be removed in v2.9.0 +- **Deprecation**: Fields without platform suffixes are legacy and should only be removed in a planned major release. ### Type System -For complete type definitions and documentation, see: +For complete type definitions and documentation, see: The library follows the OpenIAP type specifications with platform-specific extensions using iOS/Android suffixes. @@ -109,7 +109,7 @@ The library follows the OpenIAP type specifications with platform-specific exten ### API Method Naming - Functions that depend on event results should use `request` prefix (e.g., `requestPurchase`) -- Follow OpenIAP terminology: +- Follow OpenIAP terminology: - Do not use generic prefixes like `get`, `find` - refer to the official terminology ## IAP-Specific Guidelines @@ -118,10 +118,10 @@ The library follows the OpenIAP type specifications with platform-specific exten All implementations must follow the OpenIAP specification: -- **APIs**: -- **Types**: -- **Events**: -- **Errors**: +- **APIs**: +- **Types**: +- **Events**: +- **Errors**: ### Feature Development Process @@ -251,7 +251,7 @@ const {requestPurchase} = useIAP({ For complete error handling documentation, see: -- [Error Codes Reference](https://www.openiap.dev/docs/errors) +- [Error Codes Reference](https://openiap.dev/docs/errors) - [Error Handling Guide](https://docs.expo-iap.dev/docs/guides/error-handling) ## Documentation Guidelines diff --git a/libraries/expo-iap/CONTRIBUTING.md b/libraries/expo-iap/CONTRIBUTING.md index c3492e3c..1b41b97c 100644 --- a/libraries/expo-iap/CONTRIBUTING.md +++ b/libraries/expo-iap/CONTRIBUTING.md @@ -446,8 +446,8 @@ We welcome feature requests! Please: ## 📚 Additional Resources -- [Documentation Site](https://hyochan.github.io/expo-iap) -- [API Reference](https://hyochan.github.io/expo-iap/docs/api/use-iap) +- [Documentation Site](https://openiap.dev/docs/setup/expo) +- [API Reference](https://openiap.dev/docs/apis) - [Example App](./example) Thank you for contributing to expo-iap! 🎉 diff --git a/libraries/expo-iap/README.md b/libraries/expo-iap/README.md index f1f371a9..6303b56f 100644 --- a/libraries/expo-iap/README.md +++ b/libraries/expo-iap/README.md @@ -1,13 +1,13 @@ # Expo IAP
- Expo IAP Logo + Expo IAP Logo [![Version](http://img.shields.io/npm/v/expo-iap.svg?style=flat-square)](https://npmjs.org/package/expo-iap) [![Download](http://img.shields.io/npm/dm/expo-iap.svg?style=flat-square)](https://npmjs.org/package/expo-iap) [![OpenIAP](https://img.shields.io/badge/OpenIAP-Compliant-green?style=flat-square)](https://openiap.dev) [![CI](https://github.com/hyodotdev/openiap/actions/workflows/ci.yml/badge.svg)](https://github.com/hyodotdev/openiap/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/hyodotdev/openiap/graph/badge.svg?token=47VMTY5NyM)](https://codecov.io/gh/hyodotdev/openiap) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fhyochan%2Fexpo-iap.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fhyochan%2Fexpo-iap?ref=badge_shield&issueType=license) Expo IAP is a powerful in-app purchase solution for Expo and React Native applications that conforms to the Open IAP specification. It provides a unified API for handling in-app purchases across iOS and Android platforms with comprehensive error handling and modern TypeScript support. -If you're shipping an app with expo-iap, we’d love to hear about it—please share your product and feedback in [Who's using Expo IAP?](https://github.com/hyochan/expo-iap/discussions/143). Community stories help us keep improving the ecosystem. +If you're shipping an app with expo-iap, we’d love to hear about it—please share your product and feedback in [expo-iap Q&A Discussions](https://github.com/hyodotdev/openiap/discussions/categories/expo-iap). Community stories help us keep improving the ecosystem. Open IAP @@ -30,19 +30,19 @@ If you're shipping an app with expo-iap, we’d love to hear about it—please s ## 📚 Documentation -**[📖 Visit our comprehensive documentation site →](https://hyochan.github.io/expo-iap)** +**[📖 Visit our comprehensive documentation site →](https://openiap.dev/docs/setup/expo)** ## Using with AI Assistants expo-iap provides AI-friendly documentation for Cursor, GitHub Copilot, Claude, and ChatGPT. -**[📖 AI Assistants Guide →](https://hyochan.github.io/expo-iap/guides/ai-assistants)** +**[📖 AI Assistants Guide →](https://openiap.dev/docs/guides/ai-assistants)** Quick links: -- [llms.txt](https://hyochan.github.io/expo-iap/llms.txt) - Quick reference -- [llms-full.txt](https://hyochan.github.io/expo-iap/llms-full.txt) - Full API reference -- [Onside Integration](https://hyochan.github.io/expo-iap/guides/onside-integration) - Using Onside marketplace payments on iOS +- [llms.txt](https://openiap.dev/llms.txt) - Quick reference +- [llms-full.txt](https://openiap.dev/llms-full.txt) - Full API reference +- [Onside Integration](https://openiap.dev/docs/features/alternative-marketplace/onside) - Using Onside marketplace payments on iOS ## Notice @@ -53,8 +53,7 @@ The `expo-iap` module has been migrated from [react-native-iap](https://github.c Both libraries will continue to be maintained in parallel going forward. -📖 See the [Future Roadmap and Discussion](https://github.com/hyochan/react-native-iap/discussions/2754) for more details. -👉 Stay updated via the [Current Project Status comment](https://github.com/hyochan/react-native-iap/discussions/2754#discussioncomment-10510249). +📖 See the [OpenIAP discussions](https://github.com/hyodotdev/openiap/discussions) for roadmap and project status updates. ## Installation @@ -62,7 +61,7 @@ Both libraries will continue to be maintained in parallel going forward. npx expo install expo-iap ``` -For platform-specific configuration (Android Kotlin version, iOS deployment target, etc.), see the [Installation Guide](https://hyochan.github.io/expo-iap/getting-started/installation#important-for-expo-managed-workflow). +For platform-specific configuration (Android Kotlin version, iOS deployment target, etc.), see the [Installation Guide](https://openiap.dev/docs/setup/expo#installation). ## Contributing @@ -74,7 +73,7 @@ We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md) - Code style and conventions - Submitting pull requests -For detailed usage examples and error handling, see the [documentation](https://hyochan.github.io/expo-iap). +For detailed usage examples and error handling, see the [documentation](https://openiap.dev/docs/setup/expo). > Sharing your thoughts—any feedback would be greatly appreciated! @@ -107,7 +106,7 @@ For bug reports, please [open an issue](https://github.com/hyodotdev/openiap/iss
- Meta + Meta Meta
@@ -116,9 +115,9 @@ For bug reports, please [open an issue](https://github.com/hyodotdev/openiap/iss diff --git a/libraries/expo-iap/android/build.gradle b/libraries/expo-iap/android/build.gradle index c766915a..e4fd012b 100644 --- a/libraries/expo-iap/android/build.gradle +++ b/libraries/expo-iap/android/build.gradle @@ -1,10 +1,26 @@ import groovy.json.JsonSlurper +import org.jetbrains.kotlin.gradle.dsl.JvmTarget apply plugin: 'com.android.library' apply plugin: 'kotlin-android' group = 'expo.modules.iap' -version = '0.1.0' + +def resolvePackageJsonFile() { + def packageJsonFile = new File(projectDir.parentFile, 'package.json') + if (!packageJsonFile.isFile()) { + throw new GradleException("expo-iap: Unable to locate package.json") + } + return packageJsonFile +} + +def expoIapPackageJson = new JsonSlurper().parse(resolvePackageJsonFile()) +def expoIapPackageVersion = (expoIapPackageJson instanceof Map) ? expoIapPackageJson.version : null +if (!(expoIapPackageVersion instanceof String) || !expoIapPackageVersion.trim()) { + throw new GradleException("expo-iap: 'version' missing or invalid in package.json") +} +expoIapPackageVersion = expoIapPackageVersion.trim() +version = expoIapPackageVersion def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle") apply from: expoModulesCorePlugin @@ -32,6 +48,7 @@ if (!(googleVersion instanceof String) || !googleVersion.trim()) { throw new GradleException("expo-iap: 'google' version missing or invalid in openiap-versions.json") } def googleVersionString = googleVersion.trim() +apply from: project.file('openiap-android-sdk.gradle') // 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. @@ -40,26 +57,24 @@ def useManagedAndroidSdkVersions = false if (useManagedAndroidSdkVersions) { useDefaultAndroidSdkVersions() } else { - buildscript { - // Simple helper that allows the root project to override versions declared by this library. - ext.safeExtGet = { prop, fallback -> - rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback - } - } + def openIapCompileSdkVersion = openIapResolveAndroidSdkVersion('compileSdkVersion', 'compileSdk', 35) + def openIapMinSdkVersion = openIapResolveAndroidSdkVersion('minSdkVersion', 'minSdk', 23) + def openIapTargetSdkVersion = openIapResolveAndroidSdkVersion('targetSdkVersion', 'compileSdk', 35) + project.android { - compileSdkVersion safeExtGet("compileSdkVersion", 34) + compileSdk = openIapCompileSdkVersion defaultConfig { - minSdkVersion safeExtGet("minSdkVersion", 21) - targetSdkVersion safeExtGet("targetSdkVersion", 34) + minSdk = openIapMinSdkVersion + targetSdk = openIapTargetSdkVersion } } } android { - namespace "expo.modules.iap" + namespace = "expo.modules.iap" defaultConfig { - versionCode 1 - versionName "0.1.0" + 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 @@ -67,11 +82,7 @@ android { missingDimensionStrategy "platform", flavor } lintOptions { - abortOnError false - } - kotlinOptions { - jvmTarget = "17" - freeCompilerArgs += ["-Xskip-metadata-version-check"] + abortOnError = false } compileOptions { sourceCompatibility JavaVersion.VERSION_17 @@ -79,6 +90,13 @@ android { } } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + freeCompilerArgs.add("-Xskip-metadata-version-check") + } +} + dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" diff --git a/libraries/expo-iap/android/openiap-android-sdk.gradle b/libraries/expo-iap/android/openiap-android-sdk.gradle new file mode 100644 index 00000000..3d665073 --- /dev/null +++ b/libraries/expo-iap/android/openiap-android-sdk.gradle @@ -0,0 +1,30 @@ +ext.openIapReadGoogleAndroidSdkVersion = { String propertyName -> + File current = projectDir + while (current != null) { + File candidate = new File(current, 'packages/google/openiap/build.gradle.kts') + if (candidate.isFile()) { + def matcher = candidate.text =~ /(?m)^\s*${propertyName}\s*=\s*(\d+).*$/ + return matcher.find() ? matcher.group(1).toInteger() : null + } + current = current.parentFile + } + return null +} + +ext.openIapToIntegerVersion = { Object value, String label -> + if (value instanceof Number) { + return value.toInteger() + } + if (value instanceof CharSequence && value.toString() ==~ /\d+/) { + return value.toString().toInteger() + } + throw new GradleException("expo-iap: ${label} must be an integer, got ${value}") +} + +ext.openIapResolveAndroidSdkVersion = { String extName, String googlePropertyName, int fallback -> + if (rootProject.ext.has(extName)) { + return openIapToIntegerVersion(rootProject.ext.get(extName), extName) + } + def googleValue = openIapReadGoogleAndroidSdkVersion(googlePropertyName) + return googleValue ?: fallback +} diff --git a/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt b/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt index 54bdb9a5..4434c307 100644 --- a/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt +++ b/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt @@ -1,6 +1,5 @@ package expo.modules.iap -import android.util.Log import dev.hyo.openiap.AndroidSubscriptionOfferInput import dev.hyo.openiap.OpenIapError import dev.hyo.openiap.OpenIapModule @@ -15,7 +14,6 @@ import java.util.Locale import java.util.concurrent.ConcurrentLinkedQueue object ExpoIapHelper { - private const val TAG = "ExpoIapHelper" private const val MAX_BUFFERED_EVENTS = 200 fun emitOrQueue( @@ -67,7 +65,7 @@ object ExpoIapHelper { val flat = mutableMapOf() // Carry over top-level fields like type, useAlternativeBilling for ((k, v) in params) { - if (k is String && k != "request") flat[k] = v + if (k != "request") flat[k] = v } // Overlay platform-specific fields for ((k, v) in nested) { @@ -205,7 +203,7 @@ object ExpoIapHelper { runCatching { emitOrQueue(module, scope, connectionReady, pendingEvents, eventName, payload) }.onFailure { error -> - android.util.Log.e(TAG, "Failed to buffer/send $logTag", error) + ExpoIapLog.failure("buffer/send $logTag", error) val errorPayload = mapOf( "code" to fallbackErrorCode, @@ -213,7 +211,7 @@ object ExpoIapHelper { ) runCatching { emitOrQueue(module, scope, connectionReady, pendingEvents, eventPurchaseError, errorPayload) - }.onFailure { android.util.Log.e(TAG, "Failed to send error event", it) } + }.onFailure { ExpoIapLog.failure("send error event", it) } } } @@ -240,7 +238,7 @@ object ExpoIapHelper { p.toJson(), ) }.onFailure { error -> - android.util.Log.e(TAG, "Failed to buffer/send PURCHASE_UPDATED", error) + ExpoIapLog.failure("buffer/send PURCHASE_UPDATED", error) // Emit as purchase error so user knows something went wrong val errorPayload = mapOf( @@ -256,7 +254,7 @@ object ExpoIapHelper { eventPurchaseError, errorPayload, ) - }.onFailure { android.util.Log.e(TAG, "Failed to send error event", it) } + }.onFailure { ExpoIapLog.failure("send error event", it) } } } openIap.addPurchaseErrorListener { e -> @@ -271,7 +269,7 @@ object ExpoIapHelper { errorJson, ) }.onFailure { error -> - android.util.Log.e(TAG, "Failed to buffer/send PURCHASE_ERROR", error) + ExpoIapLog.failure("buffer/send PURCHASE_ERROR", error) // Critical: if we can't emit the original error, at least try to emit a generic one val fallbackPayload = mapOf( @@ -287,7 +285,7 @@ object ExpoIapHelper { eventPurchaseError, fallbackPayload, ) - }.onFailure { android.util.Log.e(TAG, "Failed to send fallback error event", it) } + }.onFailure { ExpoIapLog.failure("send fallback error event", it) } } // Also reject any pending purchase promises to match iOS behavior val errorCode = errorJson["code"] as? String ?: OpenIapError.PurchaseFailed.CODE 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 d41e948e..4c124c2b 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 @@ -33,7 +33,9 @@ internal object ExpoIapLog { } fun debug(message: String) { - Log.d(TAG, message) + if (BuildConfig.DEBUG || Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, message) + } } private fun stringify(value: Any?): String { 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 a5c645f9..2f08fb94 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 @@ -1,7 +1,6 @@ package expo.modules.iap import android.content.Context -import android.util.Log import dev.hyo.openiap.AndroidSubscriptionOfferInput import dev.hyo.openiap.DeepLinkOptions import dev.hyo.openiap.FetchProductsResultProducts @@ -143,7 +142,7 @@ class ExpoIapModule : Module() { val ev = pendingEvents.poll() ?: break // Already on main dispatcher here; emit directly runCatching { sendEvent(ev.first, ev.second) } - .onFailure { Log.e(TAG, "Failed to flush buffered event: ${ev.first}", it) } + .onFailure { ExpoIapLog.failure("flush buffered event ${ev.first}", it) } } ExpoIapLog.result("initConnection", true) @@ -354,11 +353,7 @@ class ExpoIapModule : Module() { errorMap, ) }.onFailure { ex -> - Log.e( - TAG, - "Failed to send PURCHASE_ERROR event (requestPurchase)", - ex, - ) + ExpoIapLog.failure("send PURCHASE_ERROR event requestPurchase", ex) } ExpoIapHelper.rejectPurchasePromises( errorCode, @@ -391,7 +386,10 @@ class ExpoIapModule : Module() { try { openIap.consumePurchaseAndroid(token) val response = mapOf("responseCode" to 0, "purchaseToken" to token) - ExpoIapLog.result("consumePurchaseAndroid", response) + ExpoIapLog.result( + "consumePurchaseAndroid", + response, + ) promise.resolve(response) } catch (e: Exception) { ExpoIapLog.failure("consumePurchaseAndroid", e) @@ -423,7 +421,7 @@ class ExpoIapModule : Module() { val activity = runCatching { currentActivity } .onFailure { - Log.e(TAG, "showAlternativeBillingDialogAndroid: Activity missing", it) + ExpoIapLog.failure("showAlternativeBillingDialogAndroid activity", it) }.getOrNull() ?: run { promise.reject(OpenIapError.ServiceUnavailable.CODE, "Activity not available", null) return@launch @@ -587,7 +585,10 @@ class ExpoIapModule : Module() { "billingProgram" to program, "externalTransactionToken" to result.externalTransactionToken, ) - ExpoIapLog.result("createBillingProgramReportingDetailsAndroid", response) + ExpoIapLog.result( + "createBillingProgramReportingDetailsAndroid", + response, + ) promise.resolve(response) } catch (e: Exception) { ExpoIapLog.failure("createBillingProgramReportingDetailsAndroid", e) @@ -603,7 +604,7 @@ class ExpoIapModule : Module() { val activity = runCatching { currentActivity } .onFailure { - Log.e(TAG, "launchExternalLinkAndroid: Activity missing", it) + ExpoIapLog.failure("launchExternalLinkAndroid activity", it) }.getOrNull() ?: run { promise.reject(OpenIapError.ServiceUnavailable.CODE, "Activity not available", null) return@launch diff --git a/libraries/expo-iap/android/src/main/java/expo/modules/iap/PromiseUtils.kt b/libraries/expo-iap/android/src/main/java/expo/modules/iap/PromiseUtils.kt index 1d896c85..bc79f650 100644 --- a/libraries/expo-iap/android/src/main/java/expo/modules/iap/PromiseUtils.kt +++ b/libraries/expo-iap/android/src/main/java/expo/modules/iap/PromiseUtils.kt @@ -1,6 +1,5 @@ package expo.modules.iap -import android.util.Log import dev.hyo.openiap.OpenIapError import expo.modules.kotlin.Promise @@ -54,7 +53,7 @@ fun Promise.safeResolve(value: Any?) { try { this.resolve(value) } catch (e: RuntimeException) { - Log.d(PromiseUtils.TAG, "Already consumed ${e.message}") + ExpoIapLog.debug("Already consumed ${e.message}") } } @@ -78,6 +77,6 @@ fun Promise.safeReject( try { this.reject(code, message, throwable) } catch (e: RuntimeException) { - Log.d(PromiseUtils.TAG, "Already consumed ${e.message}") + ExpoIapLog.debug("Already consumed ${e.message}") } } diff --git a/libraries/expo-iap/bun.lock b/libraries/expo-iap/bun.lock index 28f67dbf..7488173d 100644 --- a/libraries/expo-iap/bun.lock +++ b/libraries/expo-iap/bun.lock @@ -1,11 +1,12 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "expo-iap", "devDependencies": { - "@jest/globals": "^30.0.5", - "@types/jest": "^30.0.0", + "@jest/globals": "^29.7.0", + "@types/jest": "^29.5.0", "@types/react": "~19.1.7", "@types/react-test-renderer": "^19.1.0", "eslint": "8.57.0", @@ -382,25 +383,21 @@ "@jest/diff-sequences": ["@jest/diff-sequences@30.0.1", "", {}, "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw=="], - "@jest/environment": ["@jest/environment@30.0.5", "", { "dependencies": { "@jest/fake-timers": "30.0.5", "@jest/types": "30.0.5", "@types/node": "*", "jest-mock": "30.0.5" } }, "sha512-aRX7WoaWx1oaOkDQvCWImVQ8XNtdv5sEWgk4gxR6NXb7WBUnL5sRak4WRzIQRZ1VTWPvV4VI4mgGjNL9TeKMYA=="], + "@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], - "@jest/expect": ["@jest/expect@30.0.5", "", { "dependencies": { "expect": "30.0.5", "jest-snapshot": "30.0.5" } }, "sha512-6udac8KKrtTtC+AXZ2iUN/R7dp7Ydry+Fo6FPFnDG54wjVMnb6vW/XNlf7Xj8UDjAE3aAVAsR4KFyKk3TCXmTA=="], + "@jest/expect": ["@jest/expect@29.7.0", "", { "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" } }, "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ=="], - "@jest/expect-utils": ["@jest/expect-utils@30.0.5", "", { "dependencies": { "@jest/get-type": "30.0.1" } }, "sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew=="], + "@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="], - "@jest/fake-timers": ["@jest/fake-timers@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", "jest-message-util": "30.0.5", "jest-mock": "30.0.5", "jest-util": "30.0.5" } }, "sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw=="], + "@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="], "@jest/get-type": ["@jest/get-type@30.0.1", "", {}, "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw=="], - "@jest/globals": ["@jest/globals@30.0.5", "", { "dependencies": { "@jest/environment": "30.0.5", "@jest/expect": "30.0.5", "@jest/types": "30.0.5", "jest-mock": "30.0.5" } }, "sha512-7oEJT19WW4oe6HR7oLRvHxwlJk2gev0U9px3ufs8sX9PoD1Eza68KF0/tlN7X0dq/WVsBScXQGgCldA1V9Y/jA=="], - - "@jest/pattern": ["@jest/pattern@30.0.1", "", { "dependencies": { "@types/node": "*", "jest-regex-util": "30.0.1" } }, "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA=="], + "@jest/globals": ["@jest/globals@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", "@jest/types": "^29.6.3", "jest-mock": "^29.7.0" } }, "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ=="], "@jest/reporters": ["@jest/reporters@29.7.0", "", { "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", "exit": "^0.1.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "jest-worker": "^29.7.0", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", "v8-to-istanbul": "^9.0.1" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"] }, "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg=="], - "@jest/schemas": ["@jest/schemas@30.0.5", "", { "dependencies": { "@sinclair/typebox": "^0.34.0" } }, "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA=="], - - "@jest/snapshot-utils": ["@jest/snapshot-utils@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "natural-compare": "^1.4.0" } }, "sha512-XcCQ5qWHLvi29UUrowgDFvV4t7ETxX91CbDczMnoqXPOIcZOxyNdSjm6kV5XMc8+HkxfRegU/MUmnTbJRzGrUQ=="], + "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], "@jest/source-map": ["@jest/source-map@29.6.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", "graceful-fs": "^4.2.9" } }, "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw=="], @@ -410,7 +407,7 @@ "@jest/transform": ["@jest/transform@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", "write-file-atomic": "^4.0.2" } }, "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw=="], - "@jest/types": ["@jest/types@30.0.5", "", { "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", "@types/yargs": "^17.0.33", "chalk": "^4.1.2" } }, "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ=="], + "@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -466,11 +463,11 @@ "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], - "@sinclair/typebox": ["@sinclair/typebox@0.34.39", "", {}, "sha512-keEoFsevmLwAedzacnTVmra66GViRH3fhWO1M+nZ8rUgpPJyN4mcvqlGr3QMrQXx4L8KNwW0q9/BeHSEoO4teg=="], + "@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], - "@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], + "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], "@testing-library/react-native": ["@testing-library/react-native@13.2.2", "", { "dependencies": { "chalk": "^4.1.2", "jest-matcher-utils": "^30.0.2", "pretty-format": "^30.0.2", "redent": "^3.0.0" }, "peerDependencies": { "jest": ">=29.0.0", "react": ">=18.2.0", "react-native": ">=0.71", "react-test-renderer": ">=18.2.0" }, "optionalPeers": ["jest"] }, "sha512-QALF+nZ4BSXBOtUs5ljLnaHKuyR+ykakYB3RYwciSrllhgZkbUjXeGkugCxrmEtQ2BUZnYVRY7AEGboMP/hucg=="], @@ -502,7 +499,7 @@ "@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="], - "@types/jest": ["@types/jest@30.0.0", "", { "dependencies": { "expect": "^30.0.0", "pretty-format": "^30.0.0" } }, "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA=="], + "@types/jest": ["@types/jest@29.5.14", "", { "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ=="], "@types/jsdom": ["@types/jsdom@20.0.1", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0" } }, "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ=="], @@ -992,7 +989,7 @@ "exit": ["exit@0.1.2", "", {}, "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ=="], - "expect": ["expect@30.0.5", "", { "dependencies": { "@jest/expect-utils": "30.0.5", "@jest/get-type": "30.0.1", "jest-matcher-utils": "30.0.5", "jest-message-util": "30.0.5", "jest-mock": "30.0.5", "jest-util": "30.0.5" } }, "sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ=="], + "expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], "expo": ["expo@53.0.20", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "0.24.20", "@expo/config": "~11.0.13", "@expo/config-plugins": "~10.1.2", "@expo/fingerprint": "0.13.4", "@expo/metro-config": "0.20.17", "@expo/vector-icons": "^14.0.0", "babel-preset-expo": "~13.2.3", "expo-asset": "~11.1.7", "expo-constants": "~17.1.7", "expo-file-system": "~18.1.11", "expo-font": "~13.3.2", "expo-keep-awake": "~14.1.4", "expo-modules-autolinking": "2.1.14", "expo-modules-core": "2.5.0", "react-native-edge-to-edge": "1.6.0", "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli", "fingerprint": "bin/fingerprint", "expo-modules-autolinking": "bin/autolinking" } }, "sha512-Nh+HIywVy9KxT/LtH08QcXqrxtUOA9BZhsXn3KCsAYA+kNb80M8VKN8/jfQF+I6CgeKyFKJoPNsWgI0y0VBGrA=="], @@ -1268,7 +1265,7 @@ "jest-config": ["jest-config@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/test-sequencer": "^29.7.0", "@jest/types": "^29.6.3", "babel-jest": "^29.7.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "jest-circus": "^29.7.0", "jest-environment-node": "^29.7.0", "jest-get-type": "^29.6.3", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.7.0", "jest-runner": "^29.7.0", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "peerDependencies": { "@types/node": "*", "ts-node": ">=9.0.0" }, "optionalPeers": ["@types/node", "ts-node"] }, "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ=="], - "jest-diff": ["jest-diff@30.0.5", "", { "dependencies": { "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.0.1", "chalk": "^4.1.2", "pretty-format": "30.0.5" } }, "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A=="], + "jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], "jest-docblock": ["jest-docblock@29.7.0", "", { "dependencies": { "detect-newline": "^3.0.0" } }, "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g=="], @@ -1286,11 +1283,11 @@ "jest-leak-detector": ["jest-leak-detector@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw=="], - "jest-matcher-utils": ["jest-matcher-utils@30.0.5", "", { "dependencies": { "@jest/get-type": "30.0.1", "chalk": "^4.1.2", "jest-diff": "30.0.5", "pretty-format": "30.0.5" } }, "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ=="], + "jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], - "jest-message-util": ["jest-message-util@30.0.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@jest/types": "30.0.5", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", "pretty-format": "30.0.5", "slash": "^3.0.0", "stack-utils": "^2.0.6" } }, "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA=="], + "jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - "jest-mock": ["jest-mock@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "jest-util": "30.0.5" } }, "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ=="], + "jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="], "jest-pnp-resolver": ["jest-pnp-resolver@1.2.3", "", { "peerDependencies": { "jest-resolve": "*" }, "optionalPeers": ["jest-resolve"] }, "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w=="], @@ -1304,7 +1301,7 @@ "jest-runtime": ["jest-runtime@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/globals": "^29.7.0", "@jest/source-map": "^29.6.3", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.7.0", "jest-snapshot": "^29.7.0", "jest-util": "^29.7.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" } }, "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ=="], - "jest-snapshot": ["jest-snapshot@30.0.5", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", "@jest/expect-utils": "30.0.5", "@jest/get-type": "30.0.1", "@jest/snapshot-utils": "30.0.5", "@jest/transform": "30.0.5", "@jest/types": "30.0.5", "babel-preset-current-node-syntax": "^1.1.0", "chalk": "^4.1.2", "expect": "30.0.5", "graceful-fs": "^4.2.11", "jest-diff": "30.0.5", "jest-matcher-utils": "30.0.5", "jest-message-util": "30.0.5", "jest-util": "30.0.5", "pretty-format": "30.0.5", "semver": "^7.7.2", "synckit": "^0.11.8" } }, "sha512-T00dWU/Ek3LqTp4+DcW6PraVxjk28WY5Ua/s+3zUKSERZSNyxTqhDXCWKG5p2HAJ+crVQ3WJ2P9YVHpj1tkW+g=="], + "jest-snapshot": ["jest-snapshot@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/types": "^7.3.3", "@jest/expect-utils": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^29.7.0", "graceful-fs": "^4.2.9", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "natural-compare": "^1.4.0", "pretty-format": "^29.7.0", "semver": "^7.5.3" } }, "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw=="], "jest-snapshot-prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], @@ -1588,7 +1585,7 @@ "pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], - "pretty-format": ["pretty-format@30.0.5", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw=="], + "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=="], @@ -2018,8 +2015,6 @@ "@expo/cli/picomatch": ["picomatch@3.0.1", "", {}, "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag=="], - "@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/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], "@expo/cli/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], @@ -2056,46 +2051,20 @@ "@istanbuljs/load-nyc-config/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=="], - "@jest/console/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "@jest/console/jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - "@jest/console/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "@jest/core/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - "@jest/core/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], - "@jest/core/jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - - "@jest/core/jest-snapshot": ["jest-snapshot@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/types": "^7.3.3", "@jest/expect-utils": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^29.7.0", "graceful-fs": "^4.2.9", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "natural-compare": "^1.4.0", "pretty-format": "^29.7.0", "semver": "^7.5.3" } }, "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw=="], - - "@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/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "@jest/create-cache-key-function/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "@jest/fake-timers/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], - - "@jest/pattern/jest-regex-util": ["jest-regex-util@30.0.1", "", {}, "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA=="], - - "@jest/reporters/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - "@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=="], - "@jest/reporters/jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - "@jest/reporters/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "@jest/reporters/string-length": ["string-length@4.0.2", "", { "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" } }, "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ=="], - "@jest/test-result/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - "@jest/test-sequencer/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "@jest/transform/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - "@jest/transform/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "@react-native/babel-plugin-codegen/@react-native/codegen": ["@react-native/codegen@0.79.5", "", { "dependencies": { "glob": "^7.1.1", "hermes-parser": "0.25.1", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" }, "peerDependencies": { "@babel/core": "*" } }, "sha512-FO5U1R525A1IFpJjy+KVznEinAgcs3u7IbnbRJUG9IH/MBXi2lEU2LtN+JarJ81MCfW4V2p0pg6t/3RGHFRrlQ=="], @@ -2110,6 +2079,10 @@ "@react-native/metro-babel-transformer/@react-native/babel-preset": ["@react-native/babel-preset@0.81.0", "", { "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.0", "babel-plugin-syntax-hermes-parser": "0.29.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-RKMgCUGsso/2b32kgg24lB68LJ6qr2geLoSQTbisY6Usye0uXeXCgbZZDbILIX9upL4uzU4staMldRZ0v08F1g=="], + "@testing-library/react-native/jest-matcher-utils": ["jest-matcher-utils@30.0.5", "", { "dependencies": { "@jest/get-type": "30.0.1", "chalk": "^4.1.2", "jest-diff": "30.0.5", "pretty-format": "30.0.5" } }, "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ=="], + + "@testing-library/react-native/pretty-format": ["pretty-format@30.0.5", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -2150,8 +2123,6 @@ "cosmiconfig/parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="], - "create-jest/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - "cssstyle/cssom": ["cssom@0.3.8", "", {}, "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="], "domexception/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], @@ -2190,10 +2161,6 @@ "execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "expect/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], - - "expo-module-scripts/@types/jest": ["@types/jest@29.5.14", "", { "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ=="], - "expo-module-scripts/ts-jest": ["ts-jest@29.0.5", "", { "dependencies": { "bs-logger": "0.x", "fast-json-stable-stringify": "2.x", "jest-util": "^29.0.0", "json5": "^2.2.3", "lodash.memoize": "4.x", "make-error": "1.x", "semver": "7.x", "yargs-parser": "^21.0.1" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@jest/types": "^29.0.0", "babel-jest": "^29.0.0", "jest": "^29.0.0", "typescript": ">=4.3" }, "optionalPeers": ["@babel/core", "@jest/types", "babel-jest"], "bin": { "ts-jest": "cli.js" } }, "sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA=="], "expo-modules-autolinking/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], @@ -2220,110 +2187,26 @@ "istanbul-lib-report/make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], - "jest/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "jest-circus/@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], - - "jest-circus/@jest/expect": ["@jest/expect@29.7.0", "", { "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" } }, "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ=="], - - "jest-circus/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "jest-circus/jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], - - "jest-circus/jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - - "jest-circus/jest-snapshot": ["jest-snapshot@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/types": "^7.3.3", "@jest/expect-utils": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^29.7.0", "graceful-fs": "^4.2.9", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "natural-compare": "^1.4.0", "pretty-format": "^29.7.0", "semver": "^7.5.3" } }, "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw=="], - - "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-circus/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "jest-cli/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "jest-config/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - "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/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/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "jest-each/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "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-environment-jsdom/@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], - - "jest-environment-jsdom/@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="], - - "jest-environment-jsdom/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "jest-environment-jsdom/jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="], - - "jest-environment-node/@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], - - "jest-environment-node/@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="], - - "jest-environment-node/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "jest-environment-node/jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="], - - "jest-expo/@jest/globals": ["@jest/globals@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", "@jest/types": "^29.6.3", "jest-mock": "^29.7.0" } }, "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ=="], - - "jest-expo/jest-snapshot": ["jest-snapshot@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/types": "^7.3.3", "@jest/expect-utils": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^29.7.0", "graceful-fs": "^4.2.9", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "natural-compare": "^1.4.0", "pretty-format": "^29.7.0", "semver": "^7.5.3" } }, "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw=="], - - "jest-haste-map/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "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-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "jest-mock/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], - "jest-resolve/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], "jest-resolve/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "jest-resolve-dependencies/jest-snapshot": ["jest-snapshot@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/types": "^7.3.3", "@jest/expect-utils": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^29.7.0", "graceful-fs": "^4.2.9", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "natural-compare": "^1.4.0", "pretty-format": "^29.7.0", "semver": "^7.5.3" } }, "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw=="], - - "jest-runner/@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], - - "jest-runner/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "jest-runner/jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - "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/@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], - - "jest-runtime/@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="], - - "jest-runtime/@jest/globals": ["@jest/globals@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", "@jest/types": "^29.6.3", "jest-mock": "^29.7.0" } }, "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ=="], - - "jest-runtime/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - "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-runtime/jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - - "jest-runtime/jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="], - - "jest-runtime/jest-snapshot": ["jest-snapshot@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/types": "^7.3.3", "@jest/expect-utils": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^29.7.0", "graceful-fs": "^4.2.9", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "natural-compare": "^1.4.0", "pretty-format": "^29.7.0", "semver": "^7.5.3" } }, "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw=="], - "jest-runtime/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "jest-runtime/strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], - "jest-snapshot/@jest/transform": ["@jest/transform@30.0.5", "", { "dependencies": { "@babel/core": "^7.27.4", "@jest/types": "30.0.5", "@jridgewell/trace-mapping": "^0.3.25", "babel-plugin-istanbul": "^7.0.0", "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", "jest-haste-map": "30.0.5", "jest-regex-util": "30.0.1", "jest-util": "30.0.5", "micromatch": "^4.0.8", "pirates": "^4.0.7", "slash": "^3.0.0", "write-file-atomic": "^5.0.1" } }, "sha512-Vk8amLQCmuZyy6GbBht1Jfo9RSdBtg7Lks+B0PecnjI8J+PCLQPGh7uI8Q/2wwpW2gLdiAfiHNsmekKlywULqg=="], - - "jest-snapshot/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], - - "jest-util/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "jest-validate/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "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/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "jest-watch-select-projects/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], @@ -2332,8 +2215,6 @@ "jest-watch-typeahead/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - "jest-watcher/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - "jest-watcher/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "jest-watcher/string-length": ["string-length@4.0.2", "", { "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" } }, "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ=="], @@ -2380,8 +2261,6 @@ "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/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-test-renderer/react-is": ["react-is@19.1.1", "", {}, "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA=="], "react-test-renderer/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], @@ -2446,8 +2325,6 @@ "@expo/cli/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "@expo/cli/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - "@expo/fingerprint/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@expo/metro-config/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], @@ -2462,40 +2339,10 @@ "@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "@jest/console/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "@jest/console/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/core/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - "@jest/core/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], - "@jest/core/jest-snapshot/@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="], - - "@jest/core/jest-snapshot/expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], - - "@jest/core/jest-snapshot/jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], - - "@jest/core/jest-snapshot/jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], - - "@jest/core/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "@jest/create-cache-key-function/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "@jest/fake-timers/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], - - "@jest/fake-timers/jest-util/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "@jest/reporters/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "@jest/reporters/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/reporters/string-length/char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], - "@jest/test-result/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "@jest/transform/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - "@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.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], @@ -2508,6 +2355,10 @@ "@react-native/metro-babel-transformer/@react-native/babel-preset/@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.81.0", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.81.0" } }, "sha512-MEMlW91+2Kk9GiObRP1Nc6oTdiyvmSEbPMSC6kzUzDyouxnh5/x28uyNySmB2nb6ivcbmQ0lxaU059+CZSkKXQ=="], + "@testing-library/react-native/jest-matcher-utils/jest-diff": ["jest-diff@30.0.5", "", { "dependencies": { "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.0.1", "chalk": "^4.1.2", "pretty-format": "30.0.5" } }, "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A=="], + + "@testing-library/react-native/pretty-format/@jest/schemas": ["@jest/schemas@30.0.5", "", { "dependencies": { "@sinclair/typebox": "^0.34.0" } }, "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -2526,8 +2377,6 @@ "cosmiconfig/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "create-jest/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - "eslint-plugin-expo/eslint/@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], "eslint-plugin-expo/eslint/@eslint/js": ["@eslint/js@9.33.0", "", {}, "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A=="], @@ -2540,166 +2389,20 @@ "eslint-plugin-expo/eslint/file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - "expect/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], - - "expect/jest-util/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "expo-module-scripts/@types/jest/expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], - - "expo-module-scripts/@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=="], - - "expo-module-scripts/ts-jest/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "jest-circus/@jest/environment/@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="], - - "jest-circus/@jest/environment/jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="], - - "jest-circus/@jest/expect/expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], - - "jest-circus/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-circus/jest-matcher-utils/jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], - - "jest-circus/jest-snapshot/@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="], - - "jest-circus/jest-snapshot/expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], - - "jest-circus/jest-snapshot/jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], - - "jest-circus/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-cli/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-config/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-config/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-each/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-each/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-environment-jsdom/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], - - "jest-environment-jsdom/@jest/fake-timers/jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - - "jest-environment-jsdom/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-environment-node/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], - - "jest-environment-node/@jest/fake-timers/jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - - "jest-environment-node/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-expo/@jest/globals/@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], - - "jest-expo/@jest/globals/@jest/expect": ["@jest/expect@29.7.0", "", { "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" } }, "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ=="], - - "jest-expo/@jest/globals/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "jest-expo/@jest/globals/jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="], - - "jest-expo/jest-snapshot/@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="], - - "jest-expo/jest-snapshot/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "jest-expo/jest-snapshot/expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], - - "jest-expo/jest-snapshot/jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], - - "jest-expo/jest-snapshot/jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], - - "jest-expo/jest-snapshot/jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - - "jest-expo/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-haste-map/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-leak-detector/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-mock/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], - - "jest-mock/jest-util/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "jest-resolve-dependencies/jest-snapshot/@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="], - - "jest-resolve-dependencies/jest-snapshot/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "jest-resolve-dependencies/jest-snapshot/expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], - - "jest-resolve-dependencies/jest-snapshot/jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], - - "jest-resolve-dependencies/jest-snapshot/jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], - - "jest-resolve-dependencies/jest-snapshot/jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - - "jest-resolve-dependencies/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-runner/@jest/environment/@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="], - - "jest-runner/@jest/environment/jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="], - - "jest-runner/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-runner/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-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "jest-runtime/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], - - "jest-runtime/@jest/globals/@jest/expect": ["@jest/expect@29.7.0", "", { "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" } }, "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ=="], - - "jest-runtime/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-runtime/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-runtime/jest-snapshot/@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="], - - "jest-runtime/jest-snapshot/expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], - - "jest-runtime/jest-snapshot/jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], - - "jest-runtime/jest-snapshot/jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], - - "jest-runtime/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/@jest/transform/babel-plugin-istanbul": ["babel-plugin-istanbul@7.0.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.3", "istanbul-lib-instrument": "^6.0.2", "test-exclude": "^6.0.0" } }, "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw=="], - - "jest-snapshot/@jest/transform/jest-haste-map": ["jest-haste-map@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "anymatch": "^3.1.3", "fb-watchman": "^2.0.2", "graceful-fs": "^4.2.11", "jest-regex-util": "30.0.1", "jest-util": "30.0.5", "jest-worker": "30.0.5", "micromatch": "^4.0.8", "walker": "^1.0.8" }, "optionalDependencies": { "fsevents": "^2.3.3" } }, "sha512-dkmlWNlsTSR0nH3nRfW5BKbqHefLZv0/6LCccG0xFCTWcJu8TuEwG+5Cm75iBfjVoockmO6J35o5gxtFSn5xeg=="], - - "jest-snapshot/@jest/transform/jest-regex-util": ["jest-regex-util@30.0.1", "", {}, "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA=="], - - "jest-snapshot/@jest/transform/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "jest-snapshot/@jest/transform/write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], - - "jest-snapshot/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], - - "jest-snapshot/jest-util/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "jest-util/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-validate/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-validate/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - "jest-watch-select-projects/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "jest-watch-select-projects/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "jest-watch-typeahead/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - "jest-watcher/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - "jest-watcher/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "jest-watcher/string-length/char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], - "jest/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - "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=="], @@ -2720,8 +2423,6 @@ "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - "react-native/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - "restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], "schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -2744,128 +2445,20 @@ "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - "@expo/cli/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - "@jest/console/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "@jest/console/jest-message-util/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "@jest/core/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "@jest/core/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "@jest/create-cache-key-function/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "@jest/reporters/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "@jest/reporters/jest-message-util/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "@jest/test-result/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "@jest/transform/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - "@react-native/babel-plugin-codegen/@react-native/codegen/hermes-parser/hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], "@react-native/babel-preset/babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], - "babel-preset-expo/babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], + "@testing-library/react-native/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.34.39", "", {}, "sha512-keEoFsevmLwAedzacnTVmra66GViRH3fhWO1M+nZ8rUgpPJyN4mcvqlGr3QMrQXx4L8KNwW0q9/BeHSEoO4teg=="], - "create-jest/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + "babel-preset-expo/babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], "eslint-plugin-expo/eslint/@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], "eslint-plugin-expo/eslint/file-entry-cache/flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - "expo-module-scripts/@types/jest/expect/@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="], - - "expo-module-scripts/@types/jest/expect/jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], - - "expo-module-scripts/@types/jest/expect/jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - - "expo-module-scripts/@types/jest/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "expo-module-scripts/ts-jest/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-circus/@jest/environment/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], - - "jest-circus/@jest/expect/expect/@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="], - - "jest-circus/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-circus/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-cli/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-config/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-config/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-each/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-each/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-environment-jsdom/@jest/fake-timers/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-environment-jsdom/@jest/fake-timers/jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "jest-environment-jsdom/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-environment-node/@jest/fake-timers/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-environment-node/@jest/fake-timers/jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "jest-environment-node/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-expo/@jest/globals/@jest/environment/@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="], - - "jest-expo/@jest/globals/@jest/expect/expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], - - "jest-expo/@jest/globals/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-expo/jest-snapshot/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-expo/jest-snapshot/jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "jest-expo/jest-snapshot/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-haste-map/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-leak-detector/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-resolve-dependencies/jest-snapshot/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-resolve-dependencies/jest-snapshot/jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "jest-resolve-dependencies/jest-snapshot/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-runner/@jest/environment/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], - - "jest-runner/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-runner/jest-message-util/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-runtime/@jest/globals/@jest/expect/expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], - - "jest-runtime/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-runtime/jest-message-util/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-runtime/jest-snapshot/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-snapshot/@jest/transform/jest-haste-map/jest-worker": ["jest-worker@30.0.5", "", { "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", "jest-util": "30.0.5", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" } }, "sha512-ojRXsWzEP16NdUuBw/4H/zkZdHOa7MMYCk4E430l+8fELeLg/mqmMlRhjL7UNZvQrDmnovWZV4DxX03fZF48fQ=="], - - "jest-util/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-validate/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-validate/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-watcher/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - "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=="], @@ -2876,108 +2469,16 @@ "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - "react-native/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - "serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - "@jest/console/jest-message-util/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "@jest/reporters/jest-message-util/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "expo-module-scripts/@types/jest/expect/jest-matcher-utils/jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], - - "expo-module-scripts/@types/jest/expect/jest-message-util/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "expo-module-scripts/@types/jest/expect/jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "expo-module-scripts/@types/jest/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "expo-module-scripts/ts-jest/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-environment-jsdom/@jest/fake-timers/jest-message-util/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-environment-node/@jest/fake-timers/jest-message-util/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-expo/@jest/globals/@jest/environment/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], - - "jest-expo/@jest/globals/@jest/environment/@jest/fake-timers/jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - - "jest-expo/@jest/globals/@jest/expect/expect/@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="], - - "jest-expo/@jest/globals/@jest/expect/expect/jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], - - "jest-expo/@jest/globals/@jest/expect/expect/jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - - "jest-expo/@jest/globals/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-expo/jest-snapshot/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-expo/jest-snapshot/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-resolve-dependencies/jest-snapshot/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-resolve-dependencies/jest-snapshot/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-runner/jest-message-util/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-runtime/@jest/globals/@jest/expect/expect/@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="], - - "jest-runtime/@jest/globals/@jest/expect/expect/jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], - - "jest-runtime/jest-message-util/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-runtime/jest-snapshot/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-snapshot/@jest/transform/jest-haste-map/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "log-symbols/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], "ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], "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-module-scripts/@types/jest/expect/jest-message-util/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-environment-jsdom/@jest/fake-timers/jest-message-util/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-environment-node/@jest/fake-timers/jest-message-util/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-expo/@jest/globals/@jest/environment/@jest/fake-timers/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-expo/@jest/globals/@jest/environment/@jest/fake-timers/jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "jest-expo/@jest/globals/@jest/expect/expect/jest-matcher-utils/jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], - - "jest-expo/@jest/globals/@jest/expect/expect/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-expo/@jest/globals/@jest/expect/expect/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-expo/@jest/globals/@jest/expect/expect/jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "jest-runtime/@jest/globals/@jest/expect/expect/jest-matcher-utils/jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], - - "jest-runtime/@jest/globals/@jest/expect/expect/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=="], - - "expo-module-scripts/@types/jest/expect/jest-message-util/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-expo/@jest/globals/@jest/environment/@jest/fake-timers/jest-message-util/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-expo/@jest/globals/@jest/expect/expect/jest-matcher-utils/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-expo/@jest/globals/@jest/expect/expect/jest-message-util/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-runtime/@jest/globals/@jest/expect/expect/jest-matcher-utils/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-expo/@jest/globals/@jest/environment/@jest/fake-timers/jest-message-util/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-expo/@jest/globals/@jest/expect/expect/jest-matcher-utils/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-expo/@jest/globals/@jest/expect/expect/jest-message-util/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "jest-runtime/@jest/globals/@jest/expect/expect/jest-matcher-utils/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], } } diff --git a/libraries/expo-iap/example/README.md b/libraries/expo-iap/example/README.md index 102ad1ef..918fa64d 100644 --- a/libraries/expo-iap/example/README.md +++ b/libraries/expo-iap/example/README.md @@ -77,7 +77,7 @@ const result = await requestPurchase({ if (isAndroidPurchaseArray(result)) { // TypeScript knows this is ProductPurchaseAndroid[] const purchase = result[0]; - console.log('Android Token:', purchase.purchaseTokenAndroid); + console.log('Android token available:', Boolean(purchase.purchaseTokenAndroid)); } else if (isIosPurchase(result)) { // TypeScript knows this is ProductPurchaseIos console.log('iOS Transaction ID:', result.transactionId); diff --git a/libraries/expo-iap/example/app/alternative-billing.tsx b/libraries/expo-iap/example/app/alternative-billing.tsx index 94807e50..4faa009e 100644 --- a/libraries/expo-iap/example/app/alternative-billing.tsx +++ b/libraries/expo-iap/example/app/alternative-billing.tsx @@ -81,7 +81,11 @@ function AlternativeBillingScreen() { enableBillingProgramAndroid: Platform.OS === 'android' ? billingProgram : undefined, onPurchaseSuccess: async (purchase: Purchase) => { - console.log('Purchase successful:', purchase); + console.log('Purchase successful:', { + productId: purchase.productId, + transactionId: purchase.id, + platform: purchase.platform, + }); setLastPurchase(purchase); setIsProcessing(false); @@ -226,9 +230,8 @@ function AlternativeBillingScreen() { try { // Step 1: Check if billing program is available - const availability = await isBillingProgramAvailableAndroid( - billingProgram, - ); + const availability = + await isBillingProgramAvailableAndroid(billingProgram); console.log('[Android] Billing program available:', availability); if (!availability.isAvailable) { @@ -256,20 +259,21 @@ function AlternativeBillingScreen() { setPurchaseResult('Getting reporting token...'); // Step 3: Get reporting details (after payment completes externally) - const details = await createBillingProgramReportingDetailsAndroid( - billingProgram, - ); - console.log('[Android] Reporting details:', details); + const details = + await createBillingProgramReportingDetailsAndroid(billingProgram); + console.log('[Android] Reporting details:', { + billingProgram: details.billingProgram, + hasExternalTransactionToken: Boolean( + details.externalTransactionToken, + ), + }); setPurchaseResult( `✅ Billing Programs API flow completed\n\nProduct: ${ product.id }\nProgram: ${ details.billingProgram - }\nToken: ${details.externalTransactionToken.substring( - 0, - 20, - )}...\n\n⚠️ Important:\n1. Report token to Google Play within 24 hours\n2. Process payment on your external site`, + }\nToken: ${details.externalTransactionToken}\n\n⚠️ Important:\n1. Report token to Google Play within 24 hours\n2. Process payment on your external site`, ); Alert.alert( @@ -423,10 +427,10 @@ function AlternativeBillingScreen() { {billingProgram === 'external-offer' ? 'External Offer' : billingProgram === 'external-payments' - ? 'External Payments' - : billingProgram === 'external-content-link' - ? 'External Content Link' - : billingProgram} + ? 'External Payments' + : billingProgram === 'external-content-link' + ? 'External Content Link' + : billingProgram} @@ -552,10 +556,10 @@ function AlternativeBillingScreen() { {isProcessing ? 'Processing...' : Platform.OS === 'ios' - ? '🛒 Buy (External URL)' - : androidBillingFlow === 'billing-programs' - ? '🛒 Buy (Billing Programs)' - : '🛒 Buy (User Choice Billing)'} + ? '🛒 Buy (External URL)' + : androidBillingFlow === 'billing-programs' + ? '🛒 Buy (Billing Programs)' + : '🛒 Buy (User Choice Billing)'} @@ -707,19 +711,19 @@ function AlternativeBillingScreen() { {program === 'external-offer' ? 'External Offer' : program === 'external-payments' - ? 'External Payments' - : program === 'external-content-link' - ? 'External Content Link' - : program} + ? 'External Payments' + : program === 'external-content-link' + ? 'External Content Link' + : program} {program === 'external-offer' ? 'For apps that offer digital content outside Google Play. Requires approval.' : program === 'external-payments' - ? 'For apps in eligible regions to use alternative payment processors.' - : program === 'external-content-link' - ? 'For linking to external content already purchased outside the app.' - : ''} + ? 'For apps in eligible regions to use alternative payment processors.' + : program === 'external-content-link' + ? 'For linking to external content already purchased outside the app.' + : ''} ))} diff --git a/libraries/expo-iap/example/app/available-purchases.tsx b/libraries/expo-iap/example/app/available-purchases.tsx index ffaa8cb7..b366cc1c 100644 --- a/libraries/expo-iap/example/app/available-purchases.tsx +++ b/libraries/expo-iap/example/app/available-purchases.tsx @@ -65,9 +65,11 @@ export default function AvailablePurchases() { finishTransaction, } = useIAP({ onPurchaseSuccess: async (purchase) => { - // Avoid logging sensitive token in console output - const {purchaseToken: _omit, ...safePurchase} = purchase as any; - console.log('[AVAILABLE-PURCHASES] Purchase successful:', safePurchase); + console.log('[AVAILABLE-PURCHASES] Purchase successful:', { + productId: purchase.productId, + transactionId: purchase.id, + platform: purchase.platform, + }); // Finish transaction like in subscription-flow await finishTransaction({ @@ -98,8 +100,9 @@ export default function AvailablePurchases() { try { await getActiveSubscriptions(); console.log( - '[AVAILABLE-PURCHASES] Active subscriptions result (state):', - activeSubscriptions, + '[AVAILABLE-PURCHASES] Active subscriptions result:', + activeSubscriptions.length, + 'items', ); } catch (error) { console.error( @@ -232,7 +235,7 @@ export default function AvailablePurchases() { console.log( '[AVAILABLE-PURCHASES] activeSubscriptions:', activeSubscriptions.length, - activeSubscriptions, + 'items', ); }, [activeSubscriptions]); @@ -427,9 +430,7 @@ export default function AvailablePurchases() { {selectedSubscription.purchaseToken && ( Purchase Token - - {selectedSubscription.purchaseToken} - + {selectedSubscription.purchaseToken} )} diff --git a/libraries/expo-iap/example/app/purchase-flow.tsx b/libraries/expo-iap/example/app/purchase-flow.tsx index 4c29194b..a34935dd 100644 --- a/libraries/expo-iap/example/app/purchase-flow.tsx +++ b/libraries/expo-iap/example/app/purchase-flow.tsx @@ -880,10 +880,10 @@ function PurchaseFlowContainer() { provider: verifyRequest.provider, iapkit: { ...(Platform.OS === 'ios' - ? {apple: {jws: `${jwsOrToken.substring(0, 50)}...`}} + ? {apple: {jws: jwsOrToken}} : { google: { - purchaseToken: `${jwsOrToken.substring(0, 50)}...`, + purchaseToken: jwsOrToken, }, }), }, diff --git a/libraries/expo-iap/example/app/subscription-flow.tsx b/libraries/expo-iap/example/app/subscription-flow.tsx index 4b141252..582ad03b 100644 --- a/libraries/expo-iap/example/app/subscription-flow.tsx +++ b/libraries/expo-iap/example/app/subscription-flow.tsx @@ -233,8 +233,8 @@ function SubscriptionFlow({ message: canUpgrade ? 'Upgrade available' : isDowngrade - ? 'Downgrade option' - : undefined, + ? 'Downgrade option' + : undefined, }; }, [getCurrentSubscription, isCancelled], @@ -770,8 +770,8 @@ function SubscriptionFlow({ {verificationMethod === 'ignore' ? '❌ None (Skip)' : verificationMethod === 'local' - ? '📱 Local (Device)' - : '☁️ IAPKit (Server)'} + ? '📱 Local (Device)' + : '☁️ IAPKit (Server)'} @@ -1544,11 +1544,11 @@ function SubscriptionFlowContainer() { isPurchased = hasValidToken || hasValidTransactionId; isRestoration = Boolean( 'originalTransactionIdentifierIOS' in purchase && - purchase.originalTransactionIdentifierIOS && - purchase.originalTransactionIdentifierIOS !== purchase.id && - 'transactionReasonIOS' in purchase && - purchase.transactionReasonIOS && - purchase.transactionReasonIOS !== 'PURCHASE', + purchase.originalTransactionIdentifierIOS && + purchase.originalTransactionIdentifierIOS !== purchase.id && + 'transactionReasonIOS' in purchase && + purchase.transactionReasonIOS && + purchase.transactionReasonIOS !== 'PURCHASE', ); console.log('iOS Purchase Analysis:'); @@ -1714,10 +1714,10 @@ function SubscriptionFlowContainer() { provider: verifyRequest.provider, iapkit: { ...(Platform.OS === 'ios' - ? {apple: {jws: `${jwsOrToken.substring(0, 50)}...`}} + ? {apple: {jws: jwsOrToken}} : { google: { - purchaseToken: `${jwsOrToken.substring(0, 50)}...`, + purchaseToken: jwsOrToken, }, }), }, @@ -1888,8 +1888,12 @@ function SubscriptionFlowContainer() { if (subscriptions.length > 0) { console.log( - 'Full subscription details:', - JSON.stringify(subscriptions, null, 2), + 'Subscription product summary:', + subscriptions.map((sub) => ({ + id: sub.id, + title: sub.title, + type: sub.type, + })), ); } }, [subscriptions]); diff --git a/libraries/expo-iap/example/app/webhook-stream.tsx b/libraries/expo-iap/example/app/webhook-stream.tsx index 9c29c137..7fbf29be 100644 --- a/libraries/expo-iap/example/app/webhook-stream.tsx +++ b/libraries/expo-iap/example/app/webhook-stream.tsx @@ -160,7 +160,7 @@ export default function WebhookStreamScreen() { base: {baseUrl} {'\n'} - api key: {apiKey ? `${apiKey.slice(0, 8)}…` : 'MISSING'} + api key: {apiKey ? 'CONFIGURED' : 'MISSING'} diff --git a/libraries/expo-iap/example/src/utils/buildPurchaseRows.ts b/libraries/expo-iap/example/src/utils/buildPurchaseRows.ts index def8968e..80649833 100644 --- a/libraries/expo-iap/example/src/utils/buildPurchaseRows.ts +++ b/libraries/expo-iap/example/src/utils/buildPurchaseRows.ts @@ -83,7 +83,11 @@ export const buildPurchaseRows = (purchase: Purchase): PurchaseDetailRow[] => { if (platform === 'ios') { const iosPurchase = purchase as PurchaseIOS; pushRow(rows, 'quantityIOS', iosPurchase.quantityIOS); - pushRow(rows, 'appAccountToken', iosPurchase.appAccountToken); + pushRow( + rows, + 'appAccountToken', + iosPurchase.appAccountToken ?? null, + ); pushRow(rows, 'appBundleIdIOS', iosPurchase.appBundleIdIOS); pushRow(rows, 'countryCodeIOS', iosPurchase.countryCodeIOS); pushRow(rows, 'currencyCodeIOS', iosPurchase.currencyCodeIOS); @@ -128,7 +132,11 @@ export const buildPurchaseRows = (purchase: Purchase): PurchaseDetailRow[] => { } } else if (platform === 'android') { const androidPurchase = purchase as PurchaseAndroid; - pushRow(rows, 'signatureAndroid', androidPurchase.signatureAndroid); + pushRow( + rows, + 'signatureAndroid', + androidPurchase.signatureAndroid ?? null, + ); pushRow(rows, 'packageNameAndroid', androidPurchase.packageNameAndroid); pushRow( rows, @@ -155,10 +163,14 @@ export const buildPurchaseRows = (purchase: Purchase): PurchaseDetailRow[] => { 'autoRenewingAndroid', formatBoolean(androidPurchase.autoRenewingAndroid), ); - pushRow(rows, 'dataAndroid', androidPurchase.dataAndroid); + pushRow( + rows, + 'dataAndroid', + androidPurchase.dataAndroid ?? null, + ); } - pushRow(rows, 'purchaseToken', purchase.purchaseToken); + pushRow(rows, 'purchaseToken', purchase.purchaseToken ?? null); return rows; }; diff --git a/libraries/expo-iap/ios/ExpoIapModule.swift b/libraries/expo-iap/ios/ExpoIapModule.swift index 1cbc807e..c6e7a2ec 100644 --- a/libraries/expo-iap/ios/ExpoIapModule.swift +++ b/libraries/expo-iap/ios/ExpoIapModule.swift @@ -174,14 +174,14 @@ public final class ExpoIapModule: Module { AsyncFunction("getReceiptIOS") { () async throws -> String in ExpoIapLog.payload("getReceiptIOS", payload: nil) let receipt = try await OpenIapModule.shared.getReceiptDataIOS() ?? "" - ExpoIapLog.result("getReceiptIOS", value: receipt) + ExpoIapLog.result("getReceiptIOS", value: "") return receipt } AsyncFunction("getReceiptDataIOS") { () async throws -> String in ExpoIapLog.payload("getReceiptDataIOS", payload: nil) let receipt = try await OpenIapModule.shared.getReceiptDataIOS() ?? "" - ExpoIapLog.result("getReceiptDataIOS", value: receipt) + ExpoIapLog.result("getReceiptDataIOS", value: "") return receipt } @@ -189,7 +189,7 @@ public final class ExpoIapModule: Module { ExpoIapLog.payload("requestReceiptRefreshIOS", payload: nil) _ = try await OpenIapModule.shared.syncIOS() let receipt = try await OpenIapModule.shared.getReceiptDataIOS() ?? "" - ExpoIapLog.result("requestReceiptRefreshIOS", value: receipt) + ExpoIapLog.result("requestReceiptRefreshIOS", value: "") return receipt } @@ -227,10 +227,15 @@ public final class ExpoIapModule: Module { return sanitized } catch let error as PurchaseError { ExpoIapLog.failure("verifyPurchase", error: error) - throw error + throw IapException.from(error) } catch { ExpoIapLog.failure("verifyPurchase", error: error) - throw PurchaseError.make(code: .receiptFailed) + throw IapException.from( + PurchaseError.make( + code: .purchaseVerificationFailed, + message: error.localizedDescription + ) + ) } } @@ -245,10 +250,15 @@ public final class ExpoIapModule: Module { return sanitized } catch let error as PurchaseError { ExpoIapLog.failure("verifyPurchaseWithProvider", error: error) - throw error + throw IapException.from(error) } catch { ExpoIapLog.failure("verifyPurchaseWithProvider", error: error) - throw PurchaseError.make(code: .receiptFailed) + throw IapException.from( + PurchaseError.make( + code: .purchaseVerificationFailed, + message: error.localizedDescription + ) + ) } } @@ -294,14 +304,14 @@ public final class ExpoIapModule: Module { AsyncFunction("requestPurchaseOnPromotedProductIOS") { () async throws -> Bool in ExpoIapLog.payload("requestPurchaseOnPromotedProductIOS", payload: nil) - let success = try await OpenIapModule.shared.requestPurchaseOnPromotedProductIOS() - ExpoIapLog.result("requestPurchaseOnPromotedProductIOS", value: success) - return success + let result = try await OpenIapModule.shared.requestPurchaseOnPromotedProductIOS() + ExpoIapLog.result("requestPurchaseOnPromotedProductIOS", value: result) + return result } AsyncFunction("getStorefront") { () async throws -> String in ExpoIapLog.payload("getStorefront", payload: nil) - let storefront = try await OpenIapModule.shared.getStorefrontIOS() + let storefront = try await OpenIapModule.shared.getStorefront() ExpoIapLog.result("getStorefront", value: storefront) return storefront } @@ -323,7 +333,7 @@ public final class ExpoIapModule: Module { AsyncFunction("getTransactionJwsIOS") { (sku: String) async throws -> String? in ExpoIapLog.payload("getTransactionJwsIOS", payload: ["sku": sku]) let jws = try await OpenIapModule.shared.getTransactionJwsIOS(sku: sku) - ExpoIapLog.result("getTransactionJwsIOS", value: jws) + ExpoIapLog.result("getTransactionJwsIOS", value: jws == nil ? nil : "") return jws } diff --git a/libraries/expo-iap/ios/onside/OnsideIapModule.swift b/libraries/expo-iap/ios/onside/OnsideIapModule.swift index 5d03796b..cf6f7d78 100644 --- a/libraries/expo-iap/ios/onside/OnsideIapModule.swift +++ b/libraries/expo-iap/ios/onside/OnsideIapModule.swift @@ -7,6 +7,7 @@ private enum OnsideEvent: String { case purchaseUpdated = "purchase-updated" case purchaseError = "purchase-error" case promotedProductIOS = "promoted-product-ios" + case subscriptionBillingIssue = "subscription-billing-issue" } private enum OnsideBridgeError: Error, LocalizedError { @@ -39,7 +40,7 @@ private enum OnsideBridgeError: Error, LocalizedError { } #if canImport(OnsideKit) -import OnsideKit +@preconcurrency import OnsideKit @available(iOS 16.0, *) @MainActor @@ -50,18 +51,14 @@ public final class ExpoIapOnsideModule: Module { private let productFetcher = OnsideProductFetcher() private var productCache: [String: OnsideProduct] = [:] - private let encoder: JSONEncoder = { - let encoder = JSONEncoder() - encoder.dateEncodingStrategy = .millisecondsSince1970 - return encoder - }() - nonisolated public func definition() -> ModuleDefinition { Name("ExpoIapOnside") Constants { var constants: [String: Any] = [:] - OpenIapSerialization.errorCodes().forEach { key, value in + let errorCodes = OpenIapSerialization.errorCodes() + constants["ERROR_CODES"] = errorCodes + errorCodes.forEach { key, value in constants[key] = value } constants["IS_ONSIDE_KIT_INSTALLED_IOS"] = true @@ -71,7 +68,8 @@ public final class ExpoIapOnsideModule: Module { Events( OnsideEvent.purchaseUpdated.rawValue, OnsideEvent.purchaseError.rawValue, - OnsideEvent.promotedProductIOS.rawValue + OnsideEvent.promotedProductIOS.rawValue, + OnsideEvent.subscriptionBillingIssue.rawValue ) OnCreate { @@ -98,6 +96,11 @@ public final class ExpoIapOnsideModule: Module { return true } + AsyncFunction("setPurchaseUpdatedListenerOptions") { (_: [String: Any]?) async throws -> Void in + // OnsideKit does not replay StoreKit 2 transactions through OpenIAP, + // so the StoreKit dedupe option is intentionally a no-op here. + } + AsyncFunction("fetchProducts") { (params: [String: Any]) async throws -> [[String: Any]] in ExpoIapLog.payload("fetchProductsOnside", payload: params) try await ensureObserverRegistered() @@ -164,10 +167,6 @@ public final class ExpoIapOnsideModule: Module { throw OnsideBridgeError.productNotFound(response.invalidProductIdentifiers.joined(separator: ", ")) } - await MainActor.run { - response.products.forEach { productCache[$0.productIdentifier] = $0 } - } - let payload: [[String: Any]] = try await MainActor.run { for p in response.products { productCache[p.productIdentifier] = p @@ -265,7 +264,11 @@ public final class ExpoIapOnsideModule: Module { return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in Task { @MainActor [weak self] in - self?.restoreContinuation = continuation + guard let self else { + continuation.resume(returning: false) + return + } + self.restoreContinuation = continuation Onside.defaultPaymentQueue().restoreCompletedTransactions { result in Task { @MainActor [weak self] in @@ -286,15 +289,45 @@ public final class ExpoIapOnsideModule: Module { } } - AsyncFunction("getStorefrontIOS") { () async throws -> String in - ExpoIapLog.payload("getStorefrontOnside", payload: nil) + AsyncFunction("getAvailableItems") { (alsoPublish: Bool, onlyIncludeActive: Bool) async throws -> [[String: Any]] in + ExpoIapLog.payload( + "getAvailableItemsOnside", + payload: [ + "alsoPublishToEventListenerIOS": alsoPublish, + "onlyIncludeActiveItemsIOS": onlyIncludeActive, + ] + ) try await ensureObserverRegistered() - let storefront = await Onside.defaultPaymentQueue().storefront?.countryCode ?? "" - ExpoIapLog.result("getStorefrontOnside", value: storefront) - return storefront + 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 + } + } + ExpoIapLog.result("getAvailableItemsOnside", value: payload) + return payload + } + + AsyncFunction("getStorefront") { () async throws -> String in + try await getOnsideStorefront() + } + + AsyncFunction("getStorefrontIOS") { () async throws -> String in + try await getOnsideStorefront() } } + private func getOnsideStorefront() async throws -> String { + ExpoIapLog.payload("getStorefrontOnside", payload: nil) + try await ensureObserverRegistered() + let storefront = await Onside.defaultPaymentQueue().storefront?.countryCode ?? "" + ExpoIapLog.result("getStorefrontOnside", value: storefront) + return storefront + } + private func ensureObserverRegistered() async throws { if !isInitialized { Onside.defaultPaymentQueue().add(observer: transactionObserver) @@ -315,24 +348,32 @@ public final class ExpoIapOnsideModule: Module { private func configureObserverCallbacks() { transactionObserver.onTransactionsUpdated = { [weak self] transactions in - guard let self = self else { return } - transactions.forEach { transaction in - self.handle(transaction: transaction) + Task { @MainActor [weak self] in + guard let self else { return } + transactions.forEach { transaction in + self.handle(transaction: transaction) + } } } transactionObserver.onRestoreFinished = { [weak self] in - guard let self else { return } - let cont = self.restoreContinuation - self.restoreContinuation = nil - cont?.resume(returning: true) + Task { @MainActor [weak self] in + guard let self else { return } + let cont = self.restoreContinuation + self.restoreContinuation = nil + cont?.resume(returning: true) + } } transactionObserver.onRestoreFailed = { [weak self] error in - guard let self else { return } - let cont = self.restoreContinuation - self.restoreContinuation = nil - cont?.resume(throwing: OnsideBridgeError.queueError(error.localizedDescription)) + Task { @MainActor [weak self] in + guard let self else { return } + let cont = self.restoreContinuation + self.restoreContinuation = nil + cont?.resume( + throwing: OnsideBridgeError.queueError(error.localizedDescription) + ) + } } } @@ -385,10 +426,11 @@ public final class ExpoIapOnsideModule: Module { let formatter = NumberFormatter() formatter.numberStyle = .currency formatter.currencyCode = product.price.currencyCode ?? "" - let formattedPrice = formatter.string(from: NSDecimalNumber(decimal: product.price.value)) ?? "\(product.price.value)" + let priceNumber = NSDecimalNumber(decimal: product.price.value) + let formattedPrice = formatter.string(from: priceNumber) ?? "\(product.price.value)" dictionary["displayPrice"] = formattedPrice dictionary["currency"] = product.price.currencyCode ?? "" - dictionary["price"] = product.price.value + dictionary["price"] = priceNumber dictionary["type"] = "in-app" dictionary["typeIOS"] = "non-consumable" dictionary["isFamilyShareableIOS"] = false @@ -430,13 +472,14 @@ public final class ExpoIapOnsideModule: Module { let priceFormatter = NumberFormatter() priceFormatter.numberStyle = .currency priceFormatter.currencyCode = product.price.currencyCode ?? "" - let formattedPrice = priceFormatter.string(from: NSDecimalNumber(decimal: product.price.value)) ?? "\(product.price.value)" + let priceNumber = NSDecimalNumber(decimal: product.price.value) + let formattedPrice = priceFormatter.string(from: priceNumber) ?? "\(product.price.value)" let jsonObject: [String: Any] = [ "id": product.productIdentifier, "title": product.localizedTitle, "description": product.localizedDescription, "price": [ - "value": product.price.value, + "value": priceNumber, "currencyCode": product.price.currencyCode ?? "", "formatted": formattedPrice, ], @@ -552,6 +595,11 @@ private final class OnsideProductFetcher: NSObject, OnsideProductsRequestDelegat guard !identifiers.isEmpty else { throw OnsideBridgeError.emptySkuList } + guard continuation == nil else { + throw OnsideBridgeError.queueError( + "A product request is already in progress." + ) + } return try await withCheckedThrowingContinuation { continuation in let request = Onside.makeProductsRequest(productIdentifiers: identifiers) @@ -563,28 +611,54 @@ private final class OnsideProductFetcher: NSObject, OnsideProductsRequestDelegat } func onsideProductsRequest(_ request: OnsideProductsRequest, didReceive response: OnsideProductsResponse) { - continuation?.resume(returning: response) - cleanup() + Task { @MainActor [weak self] in + self?.complete(.success(response)) + } } func onsideProductsRequestRequest( _ request: OnsideProductsRequest, didFailWithError error: OnsideProductsRequestError ) { - continuation?.resume(throwing: OnsideBridgeError.queueError(error.localizedDescription)) - cleanup() + Task { @MainActor [weak self] in + self?.complete( + .failure(OnsideBridgeError.queueError(error.localizedDescription)) + ) + } } func onsideProductsRequestDidFinish(_ request: OnsideProductsRequest) { - cleanup() + Task { @MainActor [weak self] in + self?.complete( + .failure( + OnsideBridgeError.queueError( + "Product request finished without a response." + ) + ) + ) + } } @MainActor + private func complete(_ result: Result) { + guard let continuation else { + cleanup() + return + } + self.continuation = nil + cleanup() + switch result { + case .success(let response): + continuation.resume(returning: response) + case .failure(let error): + continuation.resume(throwing: error) + } + } + private func cleanup() { request?.delegate = nil request?.stop() request = nil - continuation = nil } } @@ -607,7 +681,8 @@ public final class ExpoIapOnsideModule: Module { Events( OnsideEvent.purchaseUpdated.rawValue, OnsideEvent.purchaseError.rawValue, - OnsideEvent.promotedProductIOS.rawValue + OnsideEvent.promotedProductIOS.rawValue, + OnsideEvent.subscriptionBillingIssue.rawValue ) AsyncFunction("initConnection") { (_: [String: Any]?) async throws -> Bool in @@ -618,6 +693,10 @@ public final class ExpoIapOnsideModule: Module { throw OnsideBridgeError.sdkUnavailable } + AsyncFunction("setPurchaseUpdatedListenerOptions") { (_: [String: Any]?) async throws -> Void in + throw OnsideBridgeError.sdkUnavailable + } + AsyncFunction("fetchProducts") { (_: [String: Any]) async throws -> [[String: Any]] in throw OnsideBridgeError.sdkUnavailable } @@ -634,6 +713,14 @@ public final class ExpoIapOnsideModule: Module { throw OnsideBridgeError.sdkUnavailable } + AsyncFunction("getAvailableItems") { (_: Bool, _: Bool) async throws -> [[String: Any]] in + throw OnsideBridgeError.sdkUnavailable + } + + AsyncFunction("getStorefront") { () async throws -> String in + throw OnsideBridgeError.sdkUnavailable + } + AsyncFunction("getStorefrontIOS") { () async throws -> String in throw OnsideBridgeError.sdkUnavailable } diff --git a/libraries/expo-iap/plugin/src/withLocalOpenIAP.ts b/libraries/expo-iap/plugin/src/withLocalOpenIAP.ts index 3e4a023c..e59b2e9d 100644 --- a/libraries/expo-iap/plugin/src/withLocalOpenIAP.ts +++ b/libraries/expo-iap/plugin/src/withLocalOpenIAP.ts @@ -17,6 +17,64 @@ import { */ type LocalPathOption = string | {ios?: string; android?: string}; +interface AndroidGradlePluginVersions { + kotlin: string; + vanniktechMavenPublish: string; +} + +const DEFAULT_ANDROID_GRADLE_PLUGIN_VERSIONS: AndroidGradlePluginVersions = { + kotlin: '2.2.0', + vanniktechMavenPublish: '0.35.0', +}; + +const escapeRegExp = (value: string): string => + value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +const readGradlePluginVersion = ( + contents: string, + pluginId: string, +): string | null => { + const pattern = new RegExp( + `id\\("${escapeRegExp(pluginId)}"\\)\\s+version\\s+"([^"]+)"`, + ); + return pattern.exec(contents)?.[1] ?? null; +}; + +const setGradlePluginVersion = ( + contents: string, + pluginId: string, + version: string, +): string => { + const pattern = new RegExp( + `id\\("${escapeRegExp(pluginId)}"\\)\\s+version\\s+"[^"]+"`, + 'g', + ); + return contents.replace(pattern, `id("${pluginId}") version "${version}"`); +}; + +const resolveAndroidGradlePluginVersions = ( + androidModulePath: string, +): AndroidGradlePluginVersions => { + const rootBuildGradle = path.resolve( + androidModulePath, + '..', + 'build.gradle.kts', + ); + if (!fs.existsSync(rootBuildGradle)) { + return DEFAULT_ANDROID_GRADLE_PLUGIN_VERSIONS; + } + + const contents = fs.readFileSync(rootBuildGradle, 'utf8'); + const kotlin = + readGradlePluginVersion(contents, 'org.jetbrains.kotlin.android') ?? + DEFAULT_ANDROID_GRADLE_PLUGIN_VERSIONS.kotlin; + const vanniktechMavenPublish = + readGradlePluginVersion(contents, 'com.vanniktech.maven.publish') ?? + DEFAULT_ANDROID_GRADLE_PLUGIN_VERSIONS.vanniktechMavenPublish; + + return {kotlin, vanniktechMavenPublish}; +}; + // Log a message only once per Node process const logOnce = (() => { const printed = new Set(); @@ -134,14 +192,21 @@ const withLocalOpenIAP: ConfigPlugin< } return config; } + const pluginVersions = + resolveAndroidGradlePluginVersions(androidModulePath); + const settingsRoot = + ((config.modRequest as any).platformProjectRoot as string | undefined) ?? + path.join(projectRoot, 'android'); + const relativeAndroidModulePath = path + .relative(settingsRoot, androidModulePath) + .replace(/\\/g, '/'); // 1) settings.gradle: include and map projectDir const settings = config.modResults; const includeLine = "include ':openiap-google'"; - const projectDirLine = `project(':openiap-google').projectDir = new File('${androidModulePath.replace( - /\\/g, - '/', - )}')`; + const projectDirLine = `project(':openiap-google').projectDir = new File(settingsDir, '${relativeAndroidModulePath}')`; + const projectDirPattern = + /^project\(':openiap-google'\)\.projectDir\s*=.*$/gm; let contents = settings.contents ?? ''; // Ensure pluginManagement has plugin mappings required by the included module @@ -159,18 +224,34 @@ const withLocalOpenIAP: ConfigPlugin< contents, ); + contents = setGradlePluginVersion( + contents, + 'com.vanniktech.maven.publish', + pluginVersions.vanniktechMavenPublish, + ); + contents = setGradlePluginVersion( + contents, + 'org.jetbrains.kotlin.android', + pluginVersions.kotlin, + ); + contents = setGradlePluginVersion( + contents, + 'org.jetbrains.kotlin.plugin.compose', + pluginVersions.kotlin, + ); + const pluginLines: string[] = []; if (needsVannik) pluginLines.push( - ` id("com.vanniktech.maven.publish") version "0.29.0"`, + ` id("com.vanniktech.maven.publish") version "${pluginVersions.vanniktechMavenPublish}"`, ); if (needsKotlinAndroid) pluginLines.push( - ` id("org.jetbrains.kotlin.android") version "2.0.21"`, + ` id("org.jetbrains.kotlin.android") version "${pluginVersions.kotlin}"`, ); if (needsCompose) pluginLines.push( - ` id("org.jetbrains.kotlin.plugin.compose") version "2.0.21"`, + ` id("org.jetbrains.kotlin.plugin.compose") version "${pluginVersions.kotlin}"`, ); // If everything already present, skip @@ -197,14 +278,13 @@ const withLocalOpenIAP: ConfigPlugin< } }; - if ( - !/com\.vanniktech\.maven\.publish/.test(contents) || - !/org\.jetbrains\.kotlin\.android/.test(contents) - ) { - injectPluginManagement(); - } + injectPluginManagement(); if (!contents.includes(includeLine)) contents += `\n${includeLine}\n`; - if (!contents.includes(projectDirLine)) contents += `${projectDirLine}\n`; + if (projectDirPattern.test(contents)) { + contents = contents.replace(projectDirPattern, projectDirLine); + } else if (!contents.includes(projectDirLine)) { + contents += `${projectDirLine}\n`; + } settings.contents = contents; logOnce(`✅ Linked local Android module at: ${androidModulePath}`); return config; diff --git a/libraries/expo-iap/scripts/test-coverage.sh b/libraries/expo-iap/scripts/test-coverage.sh index 99c5f440..7f0cd3e1 100755 --- a/libraries/expo-iap/scripts/test-coverage.sh +++ b/libraries/expo-iap/scripts/test-coverage.sh @@ -1,4 +1,9 @@ #!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +cd "$PROJECT_ROOT" echo "Running tests with coverage..." @@ -10,6 +15,5 @@ bunx jest --coverage echo "Running example app tests..." cd example bunx jest --coverage --passWithNoTests -cd .. echo "Coverage reports generated in ./coverage and ./example/coverage" diff --git a/libraries/expo-iap/src/ExpoIapModule.ts b/libraries/expo-iap/src/ExpoIapModule.ts index 62a65ed8..35428b63 100644 --- a/libraries/expo-iap/src/ExpoIapModule.ts +++ b/libraries/expo-iap/src/ExpoIapModule.ts @@ -2,11 +2,34 @@ import {requireNativeModule, UnavailabilityError} from 'expo-modules-core'; import {installedFromOnside} from './onside'; type NativeIapModuleName = '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; + } + + if (typeof installedFromOnside !== 'string') { + return false; + } + + const normalized = installedFromOnside.trim().toLowerCase(); + return normalized === 'true' || normalized === ONSIDE_MARKETPLACE_ID; +} + +function shouldUseOnsideModule(): boolean { + return isOnsideInstallation() && !onsideModuleUnavailable; +} function getResolved(): {module: any; name: NativeIapModuleName} { - if (!cached) { + const expectedName: NativeIapModuleName = shouldUseOnsideModule() + ? 'ExpoIapOnside' + : 'ExpoIap'; + if (!cached || cached.name !== expectedName) { cached = resolveNativeModule(); } return cached; @@ -16,28 +39,21 @@ function resolveNativeModule(): { module: any; name: NativeIapModuleName; } { - const candidates: NativeIapModuleName[] = ['ExpoIapOnside', 'ExpoIap']; - - for (const name of candidates) { + if (isOnsideInstallation()) { try { - const module = requireNativeModule(name); - if (name === 'ExpoIapOnside' && !installedFromOnside) { - continue; - } - return {module, name}; + return { + module: requireNativeModule('ExpoIapOnside'), + name: 'ExpoIapOnside', + }; } catch (error) { - if (name === 'ExpoIapOnside' && isMissingModuleError(error, name)) { - continue; + if (!isMissingModuleError(error, 'ExpoIapOnside')) { + throw error; } - - throw error; + onsideModuleUnavailable = true; } } - throw new UnavailabilityError( - 'expo-iap', - 'ExpoIap native module is unavailable', - ); + return {module: requireNativeModule('ExpoIap'), name: 'ExpoIap'}; } function isMissingModuleError(error: unknown, moduleName: string): boolean { @@ -72,12 +88,37 @@ export function getNativeModule() { return getResolved().module; } +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 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 getResolved().name === 'ExpoIapOnside'; + return resolved.name === 'ExpoIapOnside'; } - return getResolved().module[prop]; + + const value = resolved.module[prop]; + if (value !== undefined || resolved.name !== 'ExpoIapOnside') { + return value; + } + + return getExpoIapFallbackModule()?.[prop]; }, }); diff --git a/libraries/expo-iap/src/__mocks__/ExpoIapModule.js b/libraries/expo-iap/src/__mocks__/ExpoIapModule.js index 435e3d15..915f38a5 100644 --- a/libraries/expo-iap/src/__mocks__/ExpoIapModule.js +++ b/libraries/expo-iap/src/__mocks__/ExpoIapModule.js @@ -1,4 +1,3 @@ -/* global jest */ const core = require('./expo-modules-core'); const nativeModule = core.requireNativeModule(); diff --git a/libraries/expo-iap/src/__mocks__/expo-modules-core.js b/libraries/expo-iap/src/__mocks__/expo-modules-core.js index 8fb9c953..18e7737f 100644 --- a/libraries/expo-iap/src/__mocks__/expo-modules-core.js +++ b/libraries/expo-iap/src/__mocks__/expo-modules-core.js @@ -15,9 +15,16 @@ const mockNativeModule = { getTransactionJwsIOS: jest.fn(), validateReceiptIOS: jest.fn(), presentCodeRedemptionSheetIOS: jest.fn(), + canPresentExternalPurchaseNoticeIOS: jest.fn(), + presentExternalPurchaseNoticeSheetIOS: jest.fn(), + presentExternalPurchaseLinkIOS: jest.fn(), getAppTransactionIOS: jest.fn(), + isEligibleForExternalPurchaseCustomLinkIOS: jest.fn(), + getExternalPurchaseCustomLinkTokenIOS: jest.fn(), + showExternalPurchaseCustomLinkNoticeIOS: jest.fn(), getPromotedProductIOS: jest.fn(), getPendingTransactionsIOS: jest.fn(), + getAllTransactionsIOS: jest.fn(), clearTransactionIOS: jest.fn(), // Common methods fetchProducts: jest.fn(), @@ -27,6 +34,7 @@ const mockNativeModule = { getActiveSubscriptions: jest.fn(), hasActiveSubscriptions: jest.fn(), getStorefront: jest.fn(), + restorePurchases: jest.fn(), finishTransaction: jest.fn(), verifyPurchase: jest.fn(), verifyPurchaseWithProvider: jest.fn(), @@ -35,7 +43,11 @@ const mockNativeModule = { setPurchaseUpdatedListenerOptions: jest.fn().mockResolvedValue(undefined), // Android-specific methods acknowledgePurchaseAndroid: jest.fn(), + consumePurchaseAndroid: jest.fn(), consumeProductAndroid: jest.fn(), + checkAlternativeBillingAvailabilityAndroid: jest.fn(), + showAlternativeBillingDialogAndroid: jest.fn(), + createAlternativeBillingTokenAndroid: jest.fn(), // Billing Programs API (8.2.0+) isBillingProgramAvailableAndroid: jest.fn(), launchExternalLinkAndroid: jest.fn(), diff --git a/libraries/expo-iap/src/__tests__/ExpoIapModule.test.ts b/libraries/expo-iap/src/__tests__/ExpoIapModule.test.ts new file mode 100644 index 00000000..532f710e --- /dev/null +++ b/libraries/expo-iap/src/__tests__/ExpoIapModule.test.ts @@ -0,0 +1,187 @@ +describe('ExpoIapModule proxy', () => { + afterEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + const loadExpoIapModule = () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports + return require('../ExpoIapModule').default; + }; + + it('does not load ExpoIapOnside when Onside is not enabled', () => { + const expoIapModule = { + ERROR_CODES: {}, + fetchProducts: jest.fn(), + }; + const requireNativeModule = jest.fn((name: string) => { + if (name === 'ExpoIap') return expoIapModule; + throw new Error(`Unexpected native module '${name}'`); + }); + + jest.doMock('../onside', () => ({ + installedFromOnside: false, + })); + jest.doMock('expo-modules-core', () => ({ + requireNativeModule, + UnavailabilityError: class UnavailabilityError extends Error {}, + })); + + const ExpoIapModule = loadExpoIapModule(); + + expect(ExpoIapModule.USING_ONSIDE_SDK).toBe(false); + expect(ExpoIapModule.fetchProducts).toBe(expoIapModule.fetchProducts); + expect(requireNativeModule).toHaveBeenCalledTimes(1); + expect(requireNativeModule).toHaveBeenCalledWith('ExpoIap'); + }); + + it('re-resolves when Onside availability changes after initial access', () => { + let installedFromOnside: boolean | null = null; + const expoIapModule = { + ERROR_CODES: {}, + fetchProducts: jest.fn(), + }; + const onsideModule = { + ERROR_CODES: {}, + fetchProducts: jest.fn(), + }; + const requireNativeModule = jest.fn((name: string) => { + if (name === 'ExpoIap') return expoIapModule; + if (name === 'ExpoIapOnside') return onsideModule; + throw new Error(`Cannot find native module '${name}'`); + }); + + jest.doMock('../onside', () => ({ + get installedFromOnside() { + return installedFromOnside; + }, + })); + jest.doMock('expo-modules-core', () => ({ + requireNativeModule, + UnavailabilityError: class UnavailabilityError extends Error {}, + })); + + const ExpoIapModule = loadExpoIapModule(); + + expect(ExpoIapModule.fetchProducts).toBe(expoIapModule.fetchProducts); + installedFromOnside = true; + expect(ExpoIapModule.USING_ONSIDE_SDK).toBe(true); + expect(ExpoIapModule.fetchProducts).toBe(onsideModule.fetchProducts); + expect(requireNativeModule.mock.calls.map(([name]) => name)).toEqual([ + 'ExpoIap', + 'ExpoIapOnside', + ]); + }); + + it('treats the Onside marketplace id as an Onside installation', () => { + const onsideModule = { + ERROR_CODES: {}, + fetchProducts: jest.fn(), + }; + const requireNativeModule = jest.fn((name: string) => { + if (name === 'ExpoIapOnside') return onsideModule; + throw new Error(`Cannot find native module '${name}'`); + }); + + jest.doMock('../onside', () => ({ + installedFromOnside: 'com.onside.marketplace-app', + })); + jest.doMock('expo-modules-core', () => ({ + requireNativeModule, + UnavailabilityError: class UnavailabilityError extends Error {}, + })); + + const ExpoIapModule = loadExpoIapModule(); + + expect(ExpoIapModule.USING_ONSIDE_SDK).toBe(true); + expect(ExpoIapModule.fetchProducts).toBe(onsideModule.fetchProducts); + expect(requireNativeModule).toHaveBeenCalledWith('ExpoIapOnside'); + }); + + it('does not repeatedly load a missing ExpoIapOnside module', () => { + const expoIapModule = { + ERROR_CODES: {}, + fetchProducts: jest.fn(), + verifyPurchase: jest.fn(), + }; + const requireNativeModule = jest.fn((name: string) => { + if (name === 'ExpoIapOnside') { + throw new Error("Cannot find native module 'ExpoIapOnside'"); + } + if (name === 'ExpoIap') return expoIapModule; + throw new Error(`Cannot find native module '${name}'`); + }); + + jest.doMock('../onside', () => ({ + installedFromOnside: true, + })); + jest.doMock('expo-modules-core', () => ({ + requireNativeModule, + UnavailabilityError: class UnavailabilityError extends Error {}, + })); + + const ExpoIapModule = loadExpoIapModule(); + + expect(ExpoIapModule.USING_ONSIDE_SDK).toBe(false); + expect(ExpoIapModule.fetchProducts).toBe(expoIapModule.fetchProducts); + expect(ExpoIapModule.verifyPurchase).toBe(expoIapModule.verifyPurchase); + expect(requireNativeModule.mock.calls.map(([name]) => name)).toEqual([ + 'ExpoIapOnside', + 'ExpoIap', + ]); + }); + + it('falls back to ExpoIap for methods missing from ExpoIapOnside', () => { + const onsideModule = { + ERROR_CODES: {}, + requestPurchase: jest.fn(), + }; + const expoIapModule = { + ERROR_CODES: {}, + getStorefront: jest.fn(), + verifyPurchase: jest.fn(), + }; + + jest.doMock('../onside', () => ({ + installedFromOnside: true, + })); + jest.doMock('expo-modules-core', () => ({ + requireNativeModule: jest.fn((name: string) => { + if (name === 'ExpoIapOnside') return onsideModule; + if (name === 'ExpoIap') return expoIapModule; + throw new Error(`Cannot find native module '${name}'`); + }), + UnavailabilityError: class UnavailabilityError extends Error {}, + })); + + const ExpoIapModule = loadExpoIapModule(); + + expect(ExpoIapModule.USING_ONSIDE_SDK).toBe(true); + expect(ExpoIapModule.requestPurchase).toBe(onsideModule.requestPurchase); + expect(ExpoIapModule.verifyPurchase).toBe(expoIapModule.verifyPurchase); + expect(ExpoIapModule.getStorefront).toBe(expoIapModule.getStorefront); + }); + + it('surfaces non-missing ExpoIap fallback errors', () => { + const onsideModule = { + ERROR_CODES: {}, + requestPurchase: jest.fn(), + }; + + jest.doMock('../onside', () => ({ + installedFromOnside: true, + })); + jest.doMock('expo-modules-core', () => ({ + requireNativeModule: jest.fn((name: string) => { + if (name === 'ExpoIapOnside') return onsideModule; + if (name === 'ExpoIap') throw new Error('native init failed'); + throw new Error(`Cannot find native module '${name}'`); + }), + UnavailabilityError: class UnavailabilityError extends Error {}, + })); + + const ExpoIapModule = loadExpoIapModule(); + + expect(() => ExpoIapModule.verifyPurchase).toThrow('native init failed'); + }); +}); diff --git a/libraries/expo-iap/src/__tests__/index.test.ts b/libraries/expo-iap/src/__tests__/index.test.ts index a812f6e8..cb614aaf 100644 --- a/libraries/expo-iap/src/__tests__/index.test.ts +++ b/libraries/expo-iap/src/__tests__/index.test.ts @@ -27,6 +27,7 @@ import { getAvailablePurchases, restorePurchases, promotedProductListenerIOS, + subscriptionBillingIssueListener, userChoiceBillingListenerAndroid, developerProvidedBillingListenerAndroid, PurchaseInput, @@ -53,6 +54,8 @@ afterAll(() => { describe('Public API (index.ts)', () => { beforeEach(() => { jest.clearAllMocks(); + (Platform as any).OS = 'ios'; + (Platform as any).select = jest.fn((obj) => obj.ios); (ExpoIapModule.getPromotedProductIOS as jest.Mock).mockResolvedValue(null); }); @@ -360,6 +363,29 @@ describe('Public API (index.ts)', () => { 'ext-txn-token-12345', ); }); + + it('subscriptionBillingIssueListener normalizes purchase platform', () => { + const addListener = (ExpoIapModule as any).addListener as jest.Mock; + const fn = jest.fn(); + subscriptionBillingIssueListener(fn); + + expect(addListener).toHaveBeenCalledWith( + OpenIapEvent.SubscriptionBillingIssue, + expect.any(Function), + ); + + const registeredCallback = addListener.mock.calls.find( + (call: any) => call[0] === OpenIapEvent.SubscriptionBillingIssue, + )?.[1]; + const purchase = { + id: 'billing-issue', + productId: 'sub.monthly', + platform: 'IOS', + } as any; + registeredCallback(purchase); + + expect(fn).toHaveBeenCalledWith({...purchase, platform: 'ios'}); + }); }); describe('connection', () => { @@ -432,7 +458,7 @@ describe('Public API (index.ts)', () => { expect(warnSpy).toHaveBeenCalledWith( '[Expo-IAP]', - "'inapp' product type is deprecated and will be removed in v3.1.0. Use 'in-app' instead.", + "'inapp' product type is deprecated and will be removed in a future major version. Use 'in-app' instead.", ); warnSpy.mockRestore(); }); @@ -836,6 +862,48 @@ describe('Public API (index.ts)', () => { expect(res).toEqual([{id: 'sub-123', platform: 'ios'}]); }); + it('iOS subscription passes advanced offer fields through', async () => { + (Platform as any).OS = 'ios'; + (ExpoIapModule.requestPurchase as jest.Mock) = jest + .fn() + .mockResolvedValue([{id: 'sub-advanced', platform: 'ios'}]); + + await requestPurchase({ + request: { + apple: { + sku: 'com.example.subscription.monthly', + introductoryOfferEligibility: true, + promotionalOfferJWS: { + offerId: 'promo-offer', + jws: 'compact-jws', + }, + winBackOffer: { + offerId: 'winback-offer', + }, + }, + }, + type: 'subs', + }); + + expect(ExpoIapModule.requestPurchase).toHaveBeenCalledWith({ + type: 'subs', + request: { + ios: { + sku: 'com.example.subscription.monthly', + introductoryOfferEligibility: true, + promotionalOfferJWS: { + offerId: 'promo-offer', + jws: 'compact-jws', + }, + winBackOffer: { + offerId: 'winback-offer', + }, + }, + }, + useAlternativeBilling: undefined, + }); + }); + it('iOS works without advancedCommerceData (optional field)', async () => { (Platform as any).OS = 'ios'; (ExpoIapModule.requestPurchase as jest.Mock) = jest @@ -923,18 +991,48 @@ describe('Public API (index.ts)', () => { it('restorePurchases performs iOS sync then fetches purchases', async () => { (Platform as any).OS = 'ios'; (Platform as any).select = (obj: any) => obj.ios; - jest.spyOn(iosMod as any, 'syncIOS').mockResolvedValue(undefined as any); + const syncSpy = jest + .spyOn(iosMod as any, 'syncIOS') + .mockResolvedValue(undefined as any); (ExpoIapModule.getAvailableItems as jest.Mock) = jest .fn() .mockResolvedValue([{id: 'legacy', transactionId: 'txn-restore'}]); await restorePurchases(); + expect(syncSpy).toHaveBeenCalledTimes(1); expect(ExpoIapModule.getAvailableItems).toHaveBeenCalledWith(false, true); }); - it('getPurchaseHistory placeholder (removed in v3)', () => { - // Removed legacy API in v3; keeping placeholder to maintain suite structure - expect(true).toBe(true); + it('restorePurchases uses native Onside restore when active', async () => { + (Platform as any).OS = 'ios'; + (Platform as any).select = (obj: any) => obj.ios; + const syncSpy = jest + .spyOn(iosMod as any, 'syncIOS') + .mockResolvedValue(undefined as any); + Object.defineProperty(ExpoIapModule, 'USING_ONSIDE_SDK', { + configurable: true, + value: true, + }); + (ExpoIapModule.restorePurchases as jest.Mock) = jest + .fn() + .mockResolvedValue(true); + (ExpoIapModule.getAvailableItems as jest.Mock) = jest + .fn() + .mockResolvedValue([{id: 'onside', transactionId: 'txn-onside'}]); + + try { + await restorePurchases(); + + expect(ExpoIapModule.restorePurchases).toHaveBeenCalledTimes(1); + expect(syncSpy).not.toHaveBeenCalled(); + expect(ExpoIapModule.getAvailableItems).toHaveBeenCalledWith( + false, + true, + ); + } finally { + delete (ExpoIapModule as any).USING_ONSIDE_SDK; + } }); + }); describe('finishTransaction', () => { @@ -1044,7 +1142,7 @@ describe('Public API (index.ts)', () => { transactionDate: Date.now(), } as any, }), - ).rejects.toThrow(/Unsupported Platform/); + ).rejects.toThrow(/Unsupported platform/); (Platform as any).OS = originalOs; }); }); @@ -1115,7 +1213,7 @@ describe('Public API (index.ts)', () => { it('validateReceipt throws on unsupported platform', async () => { (Platform as any).OS = 'web'; await expect(validateReceipt({apple: {sku: 'sku'}})).rejects.toThrow( - /Platform not supported/, + /Unsupported platform/, ); }); @@ -1163,17 +1261,17 @@ describe('Public API (index.ts)', () => { (Platform as any).OS = 'web'; await expect( requestPurchase({request: {} as any} as any), - ).rejects.toThrow(/Platform not supported/); + ).rejects.toThrow(/Unsupported platform/); }); }); - describe('getAvailablePurchases fallback', () => { - it('returns [] when Platform.select returns undefined', async () => { - const originalSelect = (Platform as any).select; - (Platform as any).select = () => undefined; - const res = await getAvailablePurchases(); - expect(res).toEqual([]); - (Platform as any).select = originalSelect; + describe('getAvailablePurchases platform support', () => { + it('rejects on unsupported platform', async () => { + (Platform as any).OS = 'web'; + + await expect(getAvailablePurchases()).rejects.toThrow( + /Unsupported platform: web/, + ); }); }); @@ -1289,6 +1387,7 @@ describe('Public API (index.ts)', () => { }); it('handles Android subscriptions with autoRenewingAndroid', async () => { + (Platform as any).OS = 'android'; const mockAndroidSubscription = [ { productId: 'premium_monthly', @@ -1309,6 +1408,14 @@ describe('Public API (index.ts)', () => { expect(result).toEqual(mockAndroidSubscription); expect(result[0].autoRenewingAndroid).toBe(false); }); + + it('rejects on unsupported platform', async () => { + (Platform as any).OS = 'web'; + + await expect(getActiveSubscriptions()).rejects.toThrow( + /Unsupported platform: web/, + ); + }); }); describe('hasActiveSubscriptions', () => { @@ -1405,6 +1512,14 @@ describe('Public API (index.ts)', () => { expect(result).toBe(false); }); + + it('rejects on unsupported platform', async () => { + (Platform as any).OS = 'web'; + + await expect(hasActiveSubscriptions()).rejects.toThrow( + /Unsupported platform: web/, + ); + }); }); describe('verifyPurchase', () => { diff --git a/libraries/expo-iap/src/index.ts b/libraries/expo-iap/src/index.ts index 89b5b7d7..32a61f19 100644 --- a/libraries/expo-iap/src/index.ts +++ b/libraries/expo-iap/src/index.ts @@ -101,6 +101,12 @@ type NativePurchaseUpdatedOptionsModule = { ) => Promise; }; +const isStorePlatform = (): boolean => + Platform.OS === 'ios' || Platform.OS === 'android'; + +const unsupportedPlatformError = (): Error => + new Error(`Unsupported platform: ${Platform.OS}`); + // Use the raw native module for listener calls — JSI HostObjects require the // real native module as `this` when calling addListener. Using a Proxy as // `this` triggers "native state unsupported on Proxy" on New Architecture / Hermes. @@ -211,14 +217,14 @@ const configurePurchaseUpdatedListenerOptionsIOS = ( }; /** - * TODO(v3.1.0): Remove legacy 'inapp' alias once downstream apps migrate to 'in-app'. + * TODO(next-major): Remove legacy 'inapp' alias once downstream apps migrate to 'in-app'. */ export type ProductTypeInput = ProductQueryType | 'inapp'; const normalizeProductType = (type?: ProductTypeInput) => { if (type === 'inapp') { ExpoIapConsole.warn( - "'inapp' product type is deprecated and will be removed in v3.1.0. Use 'in-app' instead.", + "'inapp' product type is deprecated and will be removed in a future major version. Use 'in-app' instead.", ); } @@ -441,7 +447,7 @@ export const promotedProductListenerIOS = ( * ```typescript * const subscription = userChoiceBillingListenerAndroid((details) => { * console.log('User selected alternative billing'); - * console.log('Token:', details.externalTransactionToken); + * console.log('External transaction token received; send it to your backend without logging it.'); * console.log('Products:', details.products); * * // Process payment in your system, then report token to Google @@ -480,7 +486,7 @@ export const userChoiceBillingListenerAndroid = ( * ```typescript * const subscription = developerProvidedBillingListenerAndroid(async (details) => { * console.log('User selected developer billing'); - * console.log('Token:', details.externalTransactionToken); + * console.log('External transaction token received; send it to your backend without logging it.'); * * // Process payment with your payment gateway * await processPaymentWithYourGateway(details.externalTransactionToken); @@ -564,7 +570,7 @@ export const subscriptionBillingIssueListener = ( * @remarks When using `useIAP()`, connection is auto-managed on mount/unmount — * pass options to the hook instead of calling this directly. * - * @see {@link https://www.openiap.dev/docs/apis/init-connection} + * @see {@link https://openiap.dev/docs/apis/init-connection} */ export const initConnection: MutationField<'initConnection'> = async (config) => { const result = await ExpoIapModule.initConnection(config ?? null); @@ -581,7 +587,7 @@ export const initConnection: MutationField<'initConnection'> = async (config) => /** * Close the store connection and release resources. * - * @see {@link https://www.openiap.dev/docs/apis/end-connection} + * @see {@link https://openiap.dev/docs/apis/end-connection} */ export const endConnection: MutationField<'endConnection'> = async () => { const result = await ExpoIapModule.endConnection(); @@ -612,7 +618,7 @@ export const endConnection: MutationField<'endConnection'> = async () => { * @remarks This is a regular promise-based call. Don't confuse with `request*` APIs * (`requestPurchase`), which are event-based. * - * @see {@link https://www.openiap.dev/docs/apis/fetch-products} + * @see {@link https://openiap.dev/docs/apis/fetch-products} */ export const fetchProducts: QueryField<'fetchProducts'> = async (request) => { ExpoIapConsole.debug('fetchProducts called with:', request); @@ -680,7 +686,7 @@ export const fetchProducts: QueryField<'fetchProducts'> = async (request) => { return castResult(filterAndroidItems(rawItems)); } - throw new Error('Unsupported platform'); + throw unsupportedPlatformError(); }; /** @@ -700,7 +706,7 @@ export const fetchProducts: QueryField<'fetchProducts'> = async (request) => { * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/get-available-purchases} + * @see {@link https://openiap.dev/docs/apis/get-available-purchases} */ export const getAvailablePurchases: QueryField< 'getAvailablePurchases' @@ -712,20 +718,21 @@ export const getAvailablePurchases: QueryField< includeSuspendedAndroid: options?.includeSuspendedAndroid ?? false, }; - const resolvePurchases: () => Promise = - Platform.select({ - ios: () => - ExpoIapModule.getAvailableItems( - normalizedOptions.alsoPublishToEventListenerIOS, - normalizedOptions.onlyIncludeActiveItemsIOS, - ) as Promise, - android: () => - ExpoIapModule.getAvailableItems(normalizedOptions) as Promise< - Purchase[] - >, - }) ?? (() => Promise.resolve([] as Purchase[])); - - const purchases = await resolvePurchases(); + let purchases: Purchase[]; + + if (Platform.OS === 'ios') { + purchases = (await ExpoIapModule.getAvailableItems( + normalizedOptions.alsoPublishToEventListenerIOS, + normalizedOptions.onlyIncludeActiveItemsIOS, + )) as Purchase[]; + } else if (Platform.OS === 'android') { + purchases = (await ExpoIapModule.getAvailableItems( + normalizedOptions, + )) as Purchase[]; + } else { + throw unsupportedPlatformError(); + } + return normalizePurchaseArray(purchases as Purchase[]); }; @@ -757,11 +764,15 @@ export const getAvailablePurchases: QueryField< * }); * ``` * - * @see {@link https://www.openiap.dev/docs/apis/get-active-subscriptions} + * @see {@link https://openiap.dev/docs/apis/get-active-subscriptions} */ export const getActiveSubscriptions: QueryField< 'getActiveSubscriptions' > = async (subscriptionIds) => { + if (!isStorePlatform()) { + throw unsupportedPlatformError(); + } + const result = await ExpoIapModule.getActiveSubscriptions( subscriptionIds ?? null, ); @@ -783,11 +794,15 @@ export const getActiveSubscriptions: QueryField< * const hasPremium = await hasActiveSubscriptions(['premium', 'premium_year']); * ``` * - * @see {@link https://www.openiap.dev/docs/apis/has-active-subscriptions} + * @see {@link https://openiap.dev/docs/apis/has-active-subscriptions} */ export const hasActiveSubscriptions: QueryField< 'hasActiveSubscriptions' > = async (subscriptionIds) => { + if (!isStorePlatform()) { + throw unsupportedPlatformError(); + } + return !!(await ExpoIapModule.hasActiveSubscriptions( subscriptionIds ?? null, )); @@ -796,7 +811,7 @@ export const hasActiveSubscriptions: QueryField< /** * Return the user's storefront country code. * - * @see {@link https://www.openiap.dev/docs/apis/get-storefront} + * @see {@link https://openiap.dev/docs/apis/get-storefront} */ export const getStorefront: QueryField<'getStorefront'> = async () => { if (Platform.OS !== 'ios' && Platform.OS !== 'android') { @@ -862,7 +877,7 @@ function normalizeRequestProps( * @remarks Event-based. Listen for the result via {@link purchaseUpdatedListener} / * {@link purchaseErrorListener}, or use `useIAP({ onPurchaseSuccess, onPurchaseError })`. * - * @see {@link https://www.openiap.dev/docs/apis/request-purchase} + * @see {@link https://openiap.dev/docs/apis/request-purchase} */ export const requestPurchase: MutationField<'requestPurchase'> = async ( args, @@ -885,7 +900,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( ' },\n' + ' type: "in-app"\n' + ' })\n\n' + - 'See: https://hyochan.github.io/expo-iap/docs/api/methods/core-methods#requestpurchase', + 'See: https://openiap.dev/docs/apis/request-purchase', ); } @@ -933,7 +948,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( ' },\n' + ' type: "in-app"\n' + ' })\n\n' + - 'See: https://hyochan.github.io/expo-iap/docs/api/methods/core-methods#requestpurchase', + 'See: https://openiap.dev/docs/apis/request-purchase', ); } @@ -977,7 +992,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( ' },\n' + ' type: "subs"\n' + ' })\n\n' + - 'See: https://hyochan.github.io/expo-iap/docs/api/methods/core-methods#requestpurchase', + 'See: https://openiap.dev/docs/apis/request-purchase', ); } @@ -1020,7 +1035,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( ); } - throw new Error('Platform not supported'); + throw unsupportedPlatformError(); }; /** @@ -1044,7 +1059,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( * @remarks **Critical:** Android purchases must be finalized within 3 days or Google * auto-refunds. iOS unfinished transactions replay on every app launch. * - * @see {@link https://www.openiap.dev/docs/apis/finish-transaction} + * @see {@link https://openiap.dev/docs/apis/finish-transaction} */ export const finishTransaction: MutationField<'finishTransaction'> = async ({ purchase, @@ -1076,24 +1091,33 @@ export const finishTransaction: MutationField<'finishTransaction'> = async ({ return; } - throw new Error('Unsupported Platform'); + throw unsupportedPlatformError(); }; /** * Restore completed transactions (cross-platform behavior) * - * - iOS: perform a lightweight sync to refresh transactions and ignore sync errors, + * - iOS: perform a lightweight sync, or Onside restore when OnsideKit is active, * then fetch available purchases to surface restored items to the app. * - Android: simply fetch available purchases (restoration happens via query). * * This helper triggers the refresh flows but does not return the purchases; consumers should * call `getAvailablePurchases` or rely on hook state to inspect the latest items. * - * @see {@link https://www.openiap.dev/docs/apis/restore-purchases} + * @see {@link https://openiap.dev/docs/apis/restore-purchases} */ export const restorePurchases: MutationField<'restorePurchases'> = async () => { if (Platform.OS === 'ios') { - await syncIOS().catch(() => undefined); + const nativeModule = ExpoIapModule as any; + + if ( + nativeModule.USING_ONSIDE_SDK && + typeof nativeModule.restorePurchases === 'function' + ) { + await nativeModule.restorePurchases().catch(() => undefined); + } else { + await syncIOS().catch(() => undefined); + } } await getAvailablePurchases({ @@ -1120,7 +1144,7 @@ export const restorePurchases: MutationField<'restorePurchases'> = async () => { * packageNameAndroid: 'com.example.app' * }); * - * @see {@link https://www.openiap.dev/docs/apis/deep-link-to-subscriptions} + * @see {@link https://openiap.dev/docs/apis/deep-link-to-subscriptions} */ export const deepLinkToSubscriptions: MutationField< 'deepLinkToSubscriptions' @@ -1135,7 +1159,7 @@ export const deepLinkToSubscriptions: MutationField< return; } - throw new Error(`Unsupported platform: ${Platform.OS}`); + throw unsupportedPlatformError(); }; /** @@ -1148,7 +1172,7 @@ export const deepLinkToSubscriptions: MutationField< * * @deprecated Use verifyPurchase instead * - * @see {@link https://www.openiap.dev/docs/apis/validate-receipt} + * @see {@link https://openiap.dev/docs/apis/validate-receipt} */ export const validateReceipt: MutationField<'validateReceipt'> = async ( options, @@ -1183,7 +1207,7 @@ export const validateReceipt: MutationField<'validateReceipt'> = async ( }); } - throw new Error('Platform not supported'); + throw unsupportedPlatformError(); }; /** @@ -1195,16 +1219,16 @@ export const validateReceipt: MutationField<'validateReceipt'> = async ( * @param options - Receipt validation options containing the SKU * @returns Promise resolving to receipt validation result * - * @see {@link https://www.openiap.dev/docs/features/validation#verify-purchase} + * @see {@link https://openiap.dev/docs/features/validation#verify-purchase} */ export const verifyPurchase: MutationField<'verifyPurchase'> = async ( options, ) => { - if (Platform.OS === 'ios' || Platform.OS === 'android') { - return ExpoIapModule.verifyPurchase(options); + if (!isStorePlatform()) { + throw unsupportedPlatformError(); } - throw new Error(`Unsupported platform: ${Platform.OS}`); + return ExpoIapModule.verifyPurchase(options); }; /** @@ -1232,43 +1256,45 @@ export const verifyPurchase: MutationField<'verifyPurchase'> = async ( * }); * ``` * - * @see {@link https://www.openiap.dev/docs/features/validation#verify-purchase-with-provider} + * @see {@link https://openiap.dev/docs/features/validation#verify-purchase-with-provider} */ export const verifyPurchaseWithProvider: MutationField< 'verifyPurchaseWithProvider' > = async (options) => { - if (Platform.OS === 'ios' || Platform.OS === 'android') { - // Auto-fill apiKey from config if not provided and provider is iapkit - if ( - options.provider === 'iapkit' && - options.iapkit && - !options.iapkit.apiKey - ) { - try { - // Dynamically import expo-constants to avoid hard dependency - const {default: Constants} = await import('expo-constants'); - const configApiKey = Constants.expoConfig?.extra?.iapkitApiKey; - if (configApiKey) { - options = { - ...options, - iapkit: { - ...options.iapkit, - apiKey: configApiKey, - }, - }; - } - } catch { - throw new Error( - 'expo-constants is required for auto-filling iapkitApiKey from config. ' + - 'Please install it: npx expo install expo-constants\n' + - 'Or provide apiKey directly in verifyPurchaseWithProvider options.', - ); + if (!isStorePlatform()) { + throw unsupportedPlatformError(); + } + + let resolvedOptions = options; + + if ( + resolvedOptions.provider === 'iapkit' && + resolvedOptions.iapkit && + !resolvedOptions.iapkit.apiKey + ) { + try { + // Dynamically import expo-constants to avoid hard dependency + const {default: Constants} = await import('expo-constants'); + const configApiKey = Constants.expoConfig?.extra?.iapkitApiKey; + if (typeof configApiKey === 'string' && configApiKey.length > 0) { + resolvedOptions = { + ...resolvedOptions, + iapkit: { + ...resolvedOptions.iapkit, + apiKey: configApiKey, + }, + }; } + } catch { + throw new Error( + 'expo-constants is required for auto-filling iapkitApiKey from config. ' + + 'Please install it: npx expo install expo-constants\n' + + 'Or provide apiKey directly in verifyPurchaseWithProvider options.', + ); } - return ExpoIapModule.verifyPurchaseWithProvider(options); } - throw new Error(`Unsupported platform: ${Platform.OS}`); + return ExpoIapModule.verifyPurchaseWithProvider(resolvedOptions); }; export * from './useIAP'; diff --git a/libraries/expo-iap/src/modules/android.ts b/libraries/expo-iap/src/modules/android.ts index 2bcda814..d67f12f5 100644 --- a/libraries/expo-iap/src/modules/android.ts +++ b/libraries/expo-iap/src/modules/android.ts @@ -132,7 +132,7 @@ export const validateReceiptAndroid = async ({ * (Android consumable products). Prefer using `finishTransaction` with * `isConsumable: true`, which dispatches to this under the hood. * - * @see {@link https://www.openiap.dev/docs/apis/android/consume-purchase-android} + * @see {@link https://openiap.dev/docs/apis/android/consume-purchase-android} */ export const consumePurchaseAndroid: MutationField< 'consumePurchaseAndroid' @@ -166,7 +166,7 @@ export const consumePurchaseAndroid: MutationField< * @param {string} params.token - The product's token (on Android) * @returns {Promise} * - * @see {@link https://www.openiap.dev/docs/apis/android/acknowledge-purchase-android} + * @see {@link https://openiap.dev/docs/apis/android/acknowledge-purchase-android} */ export const acknowledgePurchaseAndroid: MutationField< 'acknowledgePurchaseAndroid' @@ -218,7 +218,7 @@ export const openRedeemOfferCodeAndroid = async (): Promise => { * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/android/check-alternative-billing-availability-android} + * @see {@link https://openiap.dev/docs/apis/android/check-alternative-billing-availability-android} */ export const checkAlternativeBillingAvailabilityAndroid: MutationField< 'checkAlternativeBillingAvailabilityAndroid' @@ -250,7 +250,7 @@ export const checkAlternativeBillingAvailabilityAndroid: MutationField< * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/android/show-alternative-billing-dialog-android} + * @see {@link https://openiap.dev/docs/apis/android/show-alternative-billing-dialog-android} */ export const showAlternativeBillingDialogAndroid: MutationField< 'showAlternativeBillingDialogAndroid' @@ -282,7 +282,7 @@ export const showAlternativeBillingDialogAndroid: MutationField< * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/android/create-alternative-billing-token-android} + * @see {@link https://openiap.dev/docs/apis/android/create-alternative-billing-token-android} */ export const createAlternativeBillingTokenAndroid: MutationField< 'createAlternativeBillingTokenAndroid' @@ -309,7 +309,7 @@ export const createAlternativeBillingTokenAndroid: MutationField< * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/android/is-billing-program-available-android} + * @see {@link https://openiap.dev/docs/apis/android/is-billing-program-available-android} */ export const isBillingProgramAvailableAndroid: MutationField< 'isBillingProgramAvailableAndroid' @@ -334,7 +334,7 @@ export const isBillingProgramAvailableAndroid: MutationField< * }); * ``` * - * @see {@link https://www.openiap.dev/docs/apis/android/launch-external-link-android} + * @see {@link https://openiap.dev/docs/apis/android/launch-external-link-android} */ export const launchExternalLinkAndroid: MutationField< 'launchExternalLinkAndroid' @@ -359,7 +359,7 @@ export const launchExternalLinkAndroid: MutationField< * await reportToGooglePlay(details.externalTransactionToken); * ``` * - * @see {@link https://www.openiap.dev/docs/apis/android/create-billing-program-reporting-details-android} + * @see {@link https://openiap.dev/docs/apis/android/create-billing-program-reporting-details-android} */ export const createBillingProgramReportingDetailsAndroid: MutationField< 'createBillingProgramReportingDetailsAndroid' diff --git a/libraries/expo-iap/src/modules/ios.ts b/libraries/expo-iap/src/modules/ios.ts index 7c0a9df8..d4670c8d 100644 --- a/libraries/expo-iap/src/modules/ios.ts +++ b/libraries/expo-iap/src/modules/ios.ts @@ -54,7 +54,7 @@ export function isProductIOS( * * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/sync-ios} + * @see {@link https://openiap.dev/docs/apis/ios/sync-ios} */ export const syncIOS: MutationField<'syncIOS'> = async () => { return !!(await ExpoIapModule.syncIOS()); @@ -69,7 +69,7 @@ export const syncIOS: MutationField<'syncIOS'> = async () => { * * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios} + * @see {@link https://openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios} */ export const isEligibleForIntroOfferIOS: QueryField< 'isEligibleForIntroOfferIOS' @@ -89,7 +89,7 @@ export const isEligibleForIntroOfferIOS: QueryField< * * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/subscription-status-ios} + * @see {@link https://openiap.dev/docs/apis/ios/subscription-status-ios} */ export const subscriptionStatusIOS: QueryField< 'subscriptionStatusIOS' @@ -110,7 +110,7 @@ export const subscriptionStatusIOS: QueryField< * * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/current-entitlement-ios} + * @see {@link https://openiap.dev/docs/apis/ios/current-entitlement-ios} */ export const currentEntitlementIOS: QueryField< 'currentEntitlementIOS' @@ -131,7 +131,7 @@ export const currentEntitlementIOS: QueryField< * * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/latest-transaction-ios} + * @see {@link https://openiap.dev/docs/apis/ios/latest-transaction-ios} */ export const latestTransactionIOS: QueryField<'latestTransactionIOS'> = async ( sku, @@ -152,7 +152,7 @@ export const latestTransactionIOS: QueryField<'latestTransactionIOS'> = async ( * * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/begin-refund-request-ios} + * @see {@link https://openiap.dev/docs/apis/ios/begin-refund-request-ios} */ export const beginRefundRequestIOS: MutationField< 'beginRefundRequestIOS' @@ -173,7 +173,7 @@ export const beginRefundRequestIOS: MutationField< * * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/show-manage-subscriptions-ios} + * @see {@link https://openiap.dev/docs/apis/ios/show-manage-subscriptions-ios} */ export const showManageSubscriptionsIOS: MutationField< 'showManageSubscriptionsIOS' @@ -192,7 +192,7 @@ export const showManageSubscriptionsIOS: MutationField< * * @returns {Promise} Base64 encoded receipt data * - * @see {@link https://www.openiap.dev/docs/apis/ios/get-receipt-data-ios} + * @see {@link https://openiap.dev/docs/apis/ios/get-receipt-data-ios} */ export const getReceiptDataIOS: QueryField<'getReceiptDataIOS'> = async () => { return ExpoIapModule.getReceiptDataIOS(); @@ -212,7 +212,7 @@ export const getReceiptIOS = getReceiptDataIOS; * * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/get-storefront-ios} + * @see {@link https://openiap.dev/docs/apis/ios/get-storefront-ios} */ export const getStorefrontIOS: QueryField<'getStorefrontIOS'> = async () => { return ExpoIapModule.getStorefront(); @@ -243,7 +243,7 @@ export const requestReceiptRefreshIOS = async (): Promise => { * * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/is-transaction-verified-ios} + * @see {@link https://openiap.dev/docs/apis/ios/is-transaction-verified-ios} */ export const isTransactionVerifiedIOS: QueryField< 'isTransactionVerifiedIOS' @@ -264,7 +264,7 @@ export const isTransactionVerifiedIOS: QueryField< * * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/get-transaction-jws-ios} + * @see {@link https://openiap.dev/docs/apis/ios/get-transaction-jws-ios} */ export const getTransactionJwsIOS: QueryField<'getTransactionJwsIOS'> = async ( sku, @@ -292,7 +292,7 @@ export const getTransactionJwsIOS: QueryField<'getTransactionJwsIOS'> = async ( * latestTransaction?: Purchase; * }>} * - * @see {@link https://www.openiap.dev/docs/apis/ios/validate-receipt-ios} + * @see {@link https://openiap.dev/docs/apis/ios/validate-receipt-ios} */ const validateReceiptIOSImpl = async (props: VerifyPurchaseProps | string) => { const sku = @@ -323,7 +323,7 @@ export const validateReceiptIOS = * * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios} + * @see {@link https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios} */ export const presentCodeRedemptionSheetIOS: MutationField< 'presentCodeRedemptionSheetIOS' @@ -345,7 +345,7 @@ export const presentCodeRedemptionSheetIOS: MutationField< * @platform iOS * @since iOS 16.0 * - * @see {@link https://www.openiap.dev/docs/apis/ios/get-app-transaction-ios} + * @see {@link https://openiap.dev/docs/apis/ios/get-app-transaction-ios} */ export const getAppTransactionIOS: QueryField< 'getAppTransactionIOS' @@ -363,7 +363,7 @@ export const getAppTransactionIOS: QueryField< * * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/get-promoted-product-ios} + * @see {@link https://openiap.dev/docs/apis/ios/get-promoted-product-ios} */ export const getPromotedProductIOS: QueryField< 'getPromotedProductIOS' @@ -384,7 +384,7 @@ export const getPromotedProductIOS: QueryField< * * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios} + * @see {@link https://openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios} */ export const requestPurchaseOnPromotedProductIOS = async (): Promise => { @@ -398,7 +398,7 @@ export const requestPurchaseOnPromotedProductIOS = * @returns Promise resolving to array of pending transactions * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/get-pending-transactions-ios} + * @see {@link https://openiap.dev/docs/apis/ios/get-pending-transactions-ios} */ export const getPendingTransactionsIOS: QueryField< 'getPendingTransactionsIOS' @@ -410,7 +410,7 @@ export const getPendingTransactionsIOS: QueryField< /** * List every StoreKit transaction (finished + unfinished) for the current user. * - * @see {@link https://www.openiap.dev/docs/apis/ios/get-all-transactions-ios} + * @see {@link https://openiap.dev/docs/apis/ios/get-all-transactions-ios} */ export const getAllTransactionsIOS: QueryField< 'getAllTransactionsIOS' @@ -425,7 +425,7 @@ export const getAllTransactionsIOS: QueryField< * @returns Promise resolving when transaction is cleared * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/clear-transaction-ios} + * @see {@link https://openiap.dev/docs/apis/ios/clear-transaction-ios} */ export const clearTransactionIOS: MutationField< 'clearTransactionIOS' @@ -452,7 +452,7 @@ export const deepLinkToSubscriptionsIOS = (): Promise => * @returns Promise resolving to true if the notice sheet can be presented * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios} + * @see {@link https://openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios} */ export const canPresentExternalPurchaseNoticeIOS: QueryField< 'canPresentExternalPurchaseNoticeIOS' @@ -468,7 +468,7 @@ export const canPresentExternalPurchaseNoticeIOS: QueryField< * @returns Promise resolving to the result with action, token, and error if any * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios} + * @see {@link https://openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios} */ export const presentExternalPurchaseNoticeSheetIOS = async (): Promise => { @@ -483,7 +483,7 @@ export const presentExternalPurchaseNoticeSheetIOS = * @returns Promise resolving to the result with success status and error if any * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/present-external-purchase-link-ios} + * @see {@link https://openiap.dev/docs/apis/ios/present-external-purchase-link-ios} */ export const presentExternalPurchaseLinkIOS: MutationField< 'presentExternalPurchaseLinkIOS' @@ -500,7 +500,7 @@ export const presentExternalPurchaseLinkIOS: MutationField< * @platform iOS * @see https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/iseligible * - * @see {@link https://www.openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios} + * @see {@link https://openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios} */ export const isEligibleForExternalPurchaseCustomLinkIOS = async (): Promise => { @@ -516,7 +516,7 @@ export const isEligibleForExternalPurchaseCustomLinkIOS = * @platform iOS * @see https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) * - * @see {@link https://www.openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios} + * @see {@link https://openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios} */ export const getExternalPurchaseCustomLinkTokenIOS = async ( tokenType: ExternalPurchaseCustomLinkTokenTypeIOS, @@ -542,7 +542,7 @@ export const getExternalPurchaseCustomLinkTokenIOS = async ( * @platform iOS * @see https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/shownotice(type:) * - * @see {@link https://www.openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios} + * @see {@link https://openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios} */ export const showExternalPurchaseCustomLinkNoticeIOS = async ( noticeType: ExternalPurchaseCustomLinkNoticeTypeIOS, diff --git a/libraries/expo-iap/src/types.ts b/libraries/expo-iap/src/types.ts index fc6fef35..fcce2ade 100644 --- a/libraries/expo-iap/src/types.ts +++ b/libraries/expo-iap/src/types.ts @@ -587,12 +587,12 @@ export interface LimitedQuantityInfoAndroid { export interface Mutation { /** * Acknowledge a non-consumable purchase. Required within 3 days or Google auto-refunds. - * See: https://www.openiap.dev/docs/apis/android/acknowledge-purchase-android + * See: https://openiap.dev/docs/apis/android/acknowledge-purchase-android */ acknowledgePurchaseAndroid: Promise; /** * Present the refund request sheet (iOS 15+). See also Features → Refund. - * See: https://www.openiap.dev/docs/apis/ios/begin-refund-request-ios + * See: https://openiap.dev/docs/apis/ios/begin-refund-request-ios */ beginRefundRequestIOS?: Promise<(string | null)>; /** @@ -600,17 +600,17 @@ export interface Mutation { * * Returns true if available, false otherwise. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/check-alternative-billing-availability-android + * See: https://openiap.dev/docs/apis/android/check-alternative-billing-availability-android */ checkAlternativeBillingAvailabilityAndroid: Promise; /** * Clear pending transactions in the queue (sandbox helper). - * See: https://www.openiap.dev/docs/apis/ios/clear-transaction-ios + * See: https://openiap.dev/docs/apis/ios/clear-transaction-ios */ clearTransactionIOS: Promise; /** * Consume a consumable purchase so it can be re-bought. - * See: https://www.openiap.dev/docs/apis/android/consume-purchase-android + * See: https://openiap.dev/docs/apis/android/consume-purchase-android */ consumePurchaseAndroid: Promise; /** @@ -620,7 +620,7 @@ export interface Mutation { * * Returns token string, or null if creation failed. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/create-alternative-billing-token-android + * See: https://openiap.dev/docs/apis/android/create-alternative-billing-token-android */ createAlternativeBillingTokenAndroid?: Promise<(string | null)>; /** @@ -629,27 +629,27 @@ export interface Mutation { * * Returns external transaction token needed for reporting external transactions. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/create-billing-program-reporting-details-android + * See: https://openiap.dev/docs/apis/android/create-billing-program-reporting-details-android */ createBillingProgramReportingDetailsAndroid: Promise; /** * Open the platform's subscription management UI. - * See: https://www.openiap.dev/docs/apis/deep-link-to-subscriptions + * See: https://openiap.dev/docs/apis/deep-link-to-subscriptions */ deepLinkToSubscriptions: Promise; /** * Close the store connection and release resources. - * See: https://www.openiap.dev/docs/apis/end-connection + * See: https://openiap.dev/docs/apis/end-connection */ endConnection: Promise; /** * Complete a transaction after server-side verification. Required on Android within 3 days. - * See: https://www.openiap.dev/docs/apis/finish-transaction + * See: https://openiap.dev/docs/apis/finish-transaction */ finishTransaction: Promise; /** * Initialize the store connection. Call before any IAP API. - * See: https://www.openiap.dev/docs/apis/init-connection + * See: https://openiap.dev/docs/apis/init-connection */ initConnection: Promise; /** @@ -659,7 +659,7 @@ export interface Mutation { * Available in Google Play Billing Library 8.2.0+. * Returns availability result with isAvailable flag. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/is-billing-program-available-android + * See: https://openiap.dev/docs/apis/android/is-billing-program-available-android */ isBillingProgramAvailableAndroid: Promise; /** @@ -668,29 +668,29 @@ export interface Mutation { * * Shows Play Store dialog and optionally launches external URL. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/launch-external-link-android + * See: https://openiap.dev/docs/apis/android/launch-external-link-android */ launchExternalLinkAndroid: Promise; /** * Show the App Store offer code redemption sheet. - * See: https://www.openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ presentCodeRedemptionSheetIOS: Promise; /** * Present an external purchase link, StoreKit External (iOS 16+). - * See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-link-ios + * See: https://openiap.dev/docs/apis/ios/present-external-purchase-link-ios */ presentExternalPurchaseLinkIOS: Promise; /** * Present the external purchase notice sheet (iOS 17.4+). * Uses ExternalPurchase.presentNoticeSheet() which returns a token when the user continues. * Reference: https://developer.apple.com/documentation/storekit/externalpurchase/presentnoticesheet() - * See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios + * See: https://openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios */ presentExternalPurchaseNoticeSheetIOS: Promise; /** * Initiate a purchase or subscription flow; rely on events for final state. - * See: https://www.openiap.dev/docs/apis/request-purchase + * See: https://openiap.dev/docs/apis/request-purchase */ requestPurchase?: Promise<(Purchase | Purchase[] | null)>; /** @@ -699,13 +699,13 @@ export interface Mutation { * @deprecated Use promotedProductListenerIOS to receive the productId, * then call requestPurchase with that SKU instead. In StoreKit 2, * promoted products can be purchased directly via the standard purchase flow. - * See: https://www.openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios + * See: https://openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios * @deprecated Use promotedProductListenerIOS + requestPurchase instead */ requestPurchaseOnPromotedProductIOS: Promise; /** * Restore non-consumable and active subscription purchases. - * See: https://www.openiap.dev/docs/apis/restore-purchases + * See: https://openiap.dev/docs/apis/restore-purchases */ restorePurchases: Promise; /** @@ -714,29 +714,29 @@ export interface Mutation { * * Returns true if user accepted, false if user canceled. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/show-alternative-billing-dialog-android + * See: https://openiap.dev/docs/apis/android/show-alternative-billing-dialog-android */ showAlternativeBillingDialogAndroid: Promise; /** * Present the disclosure sheet required before linking out via ExternalPurchaseCustomLink (iOS 18.1+). * Call this after a deliberate customer interaction before linking out to external purchases. * Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/shownotice(type:) - * See: https://www.openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios + * See: https://openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios */ showExternalPurchaseCustomLinkNoticeIOS: Promise; /** * Present the manage-subscriptions sheet and return changed purchases (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/show-manage-subscriptions-ios + * See: https://openiap.dev/docs/apis/ios/show-manage-subscriptions-ios */ showManageSubscriptionsIOS: Promise; /** * Force sync transactions with the App Store (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/sync-ios + * See: https://openiap.dev/docs/apis/ios/sync-ios */ syncIOS: Promise; /** * Deprecated. Validate purchase receipts with the configured providers — use verifyPurchase instead. - * See: https://www.openiap.dev/docs/features/validation#verify-purchase + * See: https://openiap.dev/docs/features/validation#verify-purchase * @deprecated Use verifyPurchase */ validateReceipt: Promise; @@ -746,14 +746,14 @@ export interface Mutation { * + receipt/JWS metadata, VerifyPurchaseResultAndroid carries Play Store * receipt fields (no isValid), and VerifyPurchaseResultHorizon uses success. * Inspect the concrete variant before reading fields. - * See: https://www.openiap.dev/docs/features/validation#verify-purchase + * See: https://openiap.dev/docs/features/validation#verify-purchase */ verifyPurchase: Promise; /** * Verify via a managed provider without standing up your own server. The * PurchaseVerificationProvider enum currently exposes only IAPKit; platform * availability may differ by implementation. - * See: https://www.openiap.dev/docs/features/validation#verify-purchase-with-provider + * See: https://openiap.dev/docs/features/validation#verify-purchase-with-provider */ verifyPurchaseWithProvider: Promise; } @@ -1307,22 +1307,22 @@ export interface Query { /** * Check eligibility for the external purchase notice sheet (iOS 17.4+). * Uses ExternalPurchase.canPresent. - * See: https://www.openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios + * See: https://openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios */ canPresentExternalPurchaseNoticeIOS: Promise; /** * Get the user's current entitlement for a product, using StoreKit 2 (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/current-entitlement-ios + * See: https://openiap.dev/docs/apis/ios/current-entitlement-ios */ currentEntitlementIOS?: Promise<(PurchaseIOS | null)>; /** * Fetch products or subscriptions from the store. - * See: https://www.openiap.dev/docs/apis/fetch-products + * See: https://openiap.dev/docs/apis/fetch-products */ fetchProducts: Promise<(ProductOrSubscription[] | Product[] | ProductSubscription[] | null)>; /** * Get details of all currently active subscriptions (filters by subscriptionIds when provided). - * See: https://www.openiap.dev/docs/apis/get-active-subscriptions + * See: https://openiap.dev/docs/apis/get-active-subscriptions */ getActiveSubscriptions: Promise; /** @@ -1330,92 +1330,92 @@ export interface Query { * Requires the SK2ConsumableTransactionHistory Info.plist key in the host app * for finished consumables to be included (iOS 18+). * Unlike getAvailablePurchases, always returns the iOS-specific PurchaseIOS shape. - * See: https://www.openiap.dev/docs/apis/ios/get-all-transactions-ios + * See: https://openiap.dev/docs/apis/ios/get-all-transactions-ios */ getAllTransactionsIOS: Promise; /** * Fetch the app transaction (iOS 16+). - * See: https://www.openiap.dev/docs/apis/ios/get-app-transaction-ios + * See: https://openiap.dev/docs/apis/ios/get-app-transaction-ios */ getAppTransactionIOS?: Promise<(AppTransaction | null)>; /** * List active purchases for the current user. - * See: https://www.openiap.dev/docs/apis/get-available-purchases + * See: https://openiap.dev/docs/apis/get-available-purchases */ getAvailablePurchases: Promise; /** * Fetch a token for Apple's External Purchase Server reporting API (iOS 18.1+). * Use this token to report transactions made through ExternalPurchaseCustomLink. * Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) - * See: https://www.openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios + * See: https://openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios */ getExternalPurchaseCustomLinkTokenIOS: Promise; /** * List unfinished StoreKit transactions in the queue. - * See: https://www.openiap.dev/docs/apis/ios/get-pending-transactions-ios + * See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios */ getPendingTransactionsIOS: Promise; /** * Read the App Store-promoted product, if any (iOS 11+). - * See: https://www.openiap.dev/docs/apis/ios/get-promoted-product-ios + * See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios */ getPromotedProductIOS?: Promise<(ProductIOS | null)>; /** * Get base64-encoded receipt data (legacy validation). - * See: https://www.openiap.dev/docs/apis/ios/get-receipt-data-ios + * See: https://openiap.dev/docs/apis/ios/get-receipt-data-ios */ getReceiptDataIOS?: Promise<(string | null)>; /** * Return the user's storefront country code. - * See: https://www.openiap.dev/docs/apis/get-storefront + * See: https://openiap.dev/docs/apis/get-storefront */ getStorefront: Promise; /** * Deprecated. Get the current App Store storefront country code — use cross-platform getStorefront instead. - * See: https://www.openiap.dev/docs/apis/ios/get-storefront-ios + * See: https://openiap.dev/docs/apis/ios/get-storefront-ios * @deprecated Use getStorefront */ getStorefrontIOS: Promise; /** * Return the JWS string for a transaction (StoreKit 2). - * See: https://www.openiap.dev/docs/apis/ios/get-transaction-jws-ios + * See: https://openiap.dev/docs/apis/ios/get-transaction-jws-ios */ getTransactionJwsIOS?: Promise<(string | null)>; /** * Check whether the user has any active subscription. - * See: https://www.openiap.dev/docs/apis/has-active-subscriptions + * See: https://openiap.dev/docs/apis/has-active-subscriptions */ hasActiveSubscriptions: Promise; /** * Check eligibility for the custom-link variant of external purchase (iOS 18.1+). * Returns true if the app can use custom external purchase links. * Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/iseligible - * See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios + * See: https://openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios */ isEligibleForExternalPurchaseCustomLinkIOS: Promise; /** * Check intro-offer eligibility for a subscription group. - * See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios + * See: https://openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios */ isEligibleForIntroOfferIOS: Promise; /** * Check whether a transaction's JWS verification passed (StoreKit 2). - * See: https://www.openiap.dev/docs/apis/ios/is-transaction-verified-ios + * See: https://openiap.dev/docs/apis/ios/is-transaction-verified-ios */ isTransactionVerifiedIOS: Promise; /** * Get the latest verified transaction for a product, using StoreKit 2. - * See: https://www.openiap.dev/docs/apis/ios/latest-transaction-ios + * See: https://openiap.dev/docs/apis/ios/latest-transaction-ios */ latestTransactionIOS?: Promise<(PurchaseIOS | null)>; /** * Get subscription status objects from StoreKit 2 (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/subscription-status-ios + * See: https://openiap.dev/docs/apis/ios/subscription-status-ios */ subscriptionStatusIOS: Promise; /** * Deprecated. Legacy App Store receipt validation — use verifyPurchase instead. - * See: https://www.openiap.dev/docs/apis/ios/validate-receipt-ios + * See: https://openiap.dev/docs/apis/ios/validate-receipt-ios * @deprecated Use verifyPurchase */ validateReceiptIOS: Promise; diff --git a/libraries/expo-iap/src/useIAP.ts b/libraries/expo-iap/src/useIAP.ts index 4110eefb..abe03723 100644 --- a/libraries/expo-iap/src/useIAP.ts +++ b/libraries/expo-iap/src/useIAP.ts @@ -154,7 +154,7 @@ export interface UseIAPOptions { /** * React Hook for managing In-App Purchases. - * See documentation at https://hyochan.github.io/expo-iap/docs/hooks/useIAP + * See documentation at https://openiap.dev/docs/setup/expo#useIAP-hook */ export function useIAP(options?: UseIAPOptions): UseIap { const [connected, setConnected] = useState(false); @@ -281,7 +281,7 @@ export function useIAP(options?: UseIAPOptions): UseIap { * @remarks This is a regular promise-based call. Don't confuse with `request*` APIs * (`requestPurchase`), which are event-based. * - * @see {@link https://www.openiap.dev/docs/apis/fetch-products} + * @see {@link https://openiap.dev/docs/apis/fetch-products} */ const fetchProductsInternal = useCallback( async (params: { @@ -399,7 +399,7 @@ export function useIAP(options?: UseIAPOptions): UseIap { * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/get-available-purchases} + * @see {@link https://openiap.dev/docs/apis/get-available-purchases} */ const getAvailablePurchasesInternal = useCallback( async (options?: PurchaseOptions): Promise => { @@ -423,7 +423,7 @@ export function useIAP(options?: UseIAPOptions): UseIap { /** * Get details of all currently active subscriptions. * - * @see {@link https://www.openiap.dev/docs/apis/get-active-subscriptions} + * @see {@link https://openiap.dev/docs/apis/get-active-subscriptions} */ const getActiveSubscriptionsInternal = useCallback( async (subscriptionIds?: string[]): Promise => { @@ -442,7 +442,7 @@ export function useIAP(options?: UseIAPOptions): UseIap { /** * Check whether the user has any active subscription. * - * @see {@link https://www.openiap.dev/docs/apis/has-active-subscriptions} + * @see {@link https://openiap.dev/docs/apis/has-active-subscriptions} */ const hasActiveSubscriptionsInternal = useCallback( async (subscriptionIds?: string[]): Promise => { @@ -477,7 +477,7 @@ export function useIAP(options?: UseIAPOptions): UseIap { * @remarks **Critical:** Android purchases must be finalized within 3 days or Google * auto-refunds. iOS unfinished transactions replay on every app launch. * - * @see {@link https://www.openiap.dev/docs/apis/finish-transaction} + * @see {@link https://openiap.dev/docs/apis/finish-transaction} */ const finishTransaction = useCallback( async ({ @@ -520,7 +520,7 @@ export function useIAP(options?: UseIAPOptions): UseIap { * @remarks Event-based. Listen for the result via {@link purchaseUpdatedListener} / * {@link purchaseErrorListener}, or use `useIAP({ onPurchaseSuccess, onPurchaseError })`. * - * @see {@link https://www.openiap.dev/docs/apis/request-purchase} + * @see {@link https://openiap.dev/docs/apis/request-purchase} */ const requestPurchaseWithReset = useCallback( (requestObj: MutationRequestPurchaseArgs) => { @@ -551,7 +551,7 @@ export function useIAP(options?: UseIAPOptions): UseIap { /** * Restore non-consumable and active subscription purchases. * - * @see {@link https://www.openiap.dev/docs/apis/restore-purchases} + * @see {@link https://openiap.dev/docs/apis/restore-purchases} */ const restorePurchasesInternal = useCallback( async (options?: PurchaseOptions): Promise => { @@ -580,7 +580,7 @@ export function useIAP(options?: UseIAPOptions): UseIap { /** * Deprecated. Use verifyPurchase instead — same input/output shape. * - * @see {@link https://www.openiap.dev/docs/apis/validate-receipt} + * @see {@link https://openiap.dev/docs/apis/validate-receipt} */ const validateReceipt = useCallback(async (props: VerifyPurchaseProps) => { return validateReceiptInternal(props); @@ -589,7 +589,7 @@ export function useIAP(options?: UseIAPOptions): UseIap { /** * Verify a purchase against your own backend (returns isValid + raw store metadata). * - * @see {@link https://www.openiap.dev/docs/features/validation#verify-purchase} + * @see {@link https://openiap.dev/docs/features/validation#verify-purchase} */ const verifyPurchase = useCallback(async (props: VerifyPurchaseProps) => { return verifyPurchaseInternal(props); @@ -598,7 +598,7 @@ export function useIAP(options?: UseIAPOptions): UseIap { /** * Verify via a managed provider — currently only `iapkit` (IAPKit). The PurchaseVerificationProvider enum exposes no other provider literal today. * - * @see {@link https://www.openiap.dev/docs/features/validation#verify-purchase-with-provider} + * @see {@link https://openiap.dev/docs/features/validation#verify-purchase-with-provider} */ const verifyPurchaseWithProvider = useCallback( async (props: VerifyPurchaseWithProviderProps) => { diff --git a/libraries/flutter_inapp_purchase/CLAUDE.md b/libraries/flutter_inapp_purchase/CLAUDE.md index f7262d65..150cca65 100644 --- a/libraries/flutter_inapp_purchase/CLAUDE.md +++ b/libraries/flutter_inapp_purchase/CLAUDE.md @@ -143,7 +143,7 @@ This project uses Codecov with two checks: **codecov/patch** (new/modified lines ### API Method Naming - Functions that depend on event results should use `request` prefix (e.g., `requestPurchase`, `requestPurchaseWithBuilder`) -- Follow OpenIAP terminology: +- Follow OpenIAP terminology: - Do not use generic prefixes like `get`, `find` - refer to the official terminology ## IAP-Specific Guidelines @@ -152,10 +152,10 @@ This project uses Codecov with two checks: **codecov/patch** (new/modified lines All implementations must follow the OpenIAP specification: -- **APIs**: -- **Types**: -- **Events**: -- **Errors**: +- **APIs**: +- **Types**: +- **Events**: +- **Errors**: ### Feature Development Process diff --git a/libraries/flutter_inapp_purchase/CONTRIBUTING.md b/libraries/flutter_inapp_purchase/CONTRIBUTING.md index f633d1e3..ba66eaaa 100644 --- a/libraries/flutter_inapp_purchase/CONTRIBUTING.md +++ b/libraries/flutter_inapp_purchase/CONTRIBUTING.md @@ -112,45 +112,30 @@ flutter run ### Android: Use local openiap-google for debugging (optional) -By default, this plugin depends on the published artifact: +By default, this plugin depends on the published artifact version from +`openiap-versions.json`: ``` -implementation "io.github.hyochan.openiap:openiap-google:1.1.12" +implementation "io.github.hyochan.openiap:openiap-google:${openiapGoogleVersion}" ``` -If you need to debug against a local checkout of the OpenIAP Android module: +If you need to debug against the monorepo OpenIAP Android module: -1. Clone the module - - ``` - git clone https://github.com/hyodotdev/openiap-google - ``` - -2. Point Gradle to the local module (uncomment/edit paths) +1. Point Gradle to the local module. Edit `android/settings.gradle` and uncomment the lines, updating the path: ``` include ':openiap' - project(':openiap').projectDir = new File('/Users/you/path/to/openiap-google/openiap') - ``` - -3. Switch the dependency for debug builds - - Edit `android/build.gradle` dependencies to use the local project in debug only: - - ``` - // implementation "io.github.hyochan.openiap:openiap-google:1.1.12" - debugImplementation project(":openiap") - releaseImplementation "io.github.hyochan.openiap:openiap-google:1.1.12" + project(':openiap').projectDir = new File(settingsDir, '../../../packages/google/openiap') ``` -4. Sync and run +2. Sync and run. Run a Gradle sync from Android Studio or rebuild the Flutter module. - To revert, comment out the include lines in `settings.gradle` and restore the single - `implementation "io.github.hyochan.openiap:openiap-google:1.1.12"` line in `android/build.gradle`. + To revert, comment out the include lines in `settings.gradle`. No + `android/build.gradle` dependency changes are needed. ### 5. Commit Your Changes @@ -206,7 +191,7 @@ Please refer to [CLAUDE.md](./CLAUDE.md) for: ## Questions or Issues? -- For new feature proposals, start a discussion at: +- For new feature proposals, start a discussion at: - For bugs, open an issue with a clear description and reproduction steps - For questions, feel free to open a discussion diff --git a/libraries/flutter_inapp_purchase/KINDLE.md b/libraries/flutter_inapp_purchase/KINDLE.md deleted file mode 100644 index c2a95404..00000000 --- a/libraries/flutter_inapp_purchase/KINDLE.md +++ /dev/null @@ -1,61 +0,0 @@ -# Amazon Kindle Fire / Fire TV In-App Purchases Guide - -The plugin will automatically detect Amazon devices during runtime. - -## Testing In-App Purchases - -To test your purchases, you do not need to create an Amazon developer account. - -Install the Amazon App Tester (AAT) : -[Amazon App Tester](https://www.amazon.com/Amazon-App-Tester/dp/B00BN3YZM2) - -You need to create an amazon.sdktester.json file. - -Example : [amazon.sdktester.json](https://github.com/hyodotdev/openiap/blob/main/libraries/flutter_inapp_purchase/ancillary/amazon.sdktester.json) -Edit this to add your own Product Ids. - -Put this file into the kindle sdcard with : - - adb push amazon.sdktester.json /sdcard/ - -You can verify if the file is valid in the AAT and view the purchases. - -Add android.permission.INTERNET and com.amazon.device.iap.ResponseReceiver to your AndroidManifest like in the example https://github.com/hyodotdev/openiap/blob/main/libraries/flutter_inapp_purchase/example/android/app/src/main/AndroidManifest.xml. - -Now, when you make a purchase the AAT will intercept, show the purchases screen and allow you to make a purchase. Your app will think a real purchase has been made and you can test the full purchase flow. - -## Testing Live Purchases - -Add your apk into the "Live App Testing" tab. Add your IAP into the "In-App Items" tab. You must fill in your bank details first and submit your IAP so that the status is "live". - -Also with Gradle 3.4.0 or higher you need to take care of the obfuscating. To do so, create a Proguard file named "proguard-rules.pro" in the android/app folder. Into that file put the following content: - --dontwarn com.amazon.**
--keep class com.amazon.** {*;}
--keepattributes *Annotation\*
- -Then edit your build.gradle in app level like: - -build.gradle: - - buildTypes { - release { - shrinkResources false - signingConfig signingConfigs.release - minifyEnabled false - useProguard true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - -If you need more help, check out https://stackoverflow.com/questions/62833628/how-to-disable-code-obfuscation-in-flutter-release-apk and the amazon part https://developer.amazon.com/de/docs/in-app-purchasing/iap-obfuscate-the-code.html. - -Now your testers will be sent a link to the test version of your app. They can make purchases at no cost to test your app. - -## Submitting to the Amazon store - -Amazon developer accounts are free. I found the Amazon store the easiest to submit to (compared with Googles play & Apple store). Required screenshots are the same size as a Nexus 7 so that is what I used. - -I found the staff who checked my app very helpful (such as providing logcat output on request for example). Text is the same as other stores, except there is an additional up-to 10 bullet point summary of your app you can add. - -When you submit your app to the store there will be a warning that google billing is detected in your code. When you submit your app for approval you can mention in the testing instructions for the Amazon reviewer that you are using a cross-platform tool and the google IAP code is not used. I dont know if this is necessary but my app was approved anyway. diff --git a/libraries/flutter_inapp_purchase/README.md b/libraries/flutter_inapp_purchase/README.md index cbf22df9..1ad1ed64 100644 --- a/libraries/flutter_inapp_purchase/README.md +++ b/libraries/flutter_inapp_purchase/README.md @@ -1,7 +1,7 @@ # flutter_inapp_purchase
- flutter_inapp_purchase logo + flutter_inapp_purchase logo [![Pub Version](https://img.shields.io/pub/v/flutter_inapp_purchase.svg?style=flat-square)](https://pub.dartlang.org/packages/flutter_inapp_purchase) [![Flutter CI](https://github.com/hyodotdev/openiap/actions/workflows/ci.yml/badge.svg)](https://github.com/hyodotdev/openiap/actions/workflows/ci.yml) [![OpenIAP](https://img.shields.io/badge/OpenIAP-Compliant-green?style=flat-square)](https://openiap.dev) [![Coverage Status](https://codecov.io/gh/hyodotdev/openiap/branch/main/graph/badge.svg?token=WXBlKvRB2G)](https://codecov.io/gh/hyodotdev/openiap) ![License](https://img.shields.io/badge/license-MIT-blue.svg) @@ -13,15 +13,17 @@ ## 📚 Documentation -**[📖 Visit our comprehensive documentation site →](https://hyochan.github.io/flutter_inapp_purchase)** +**[📖 Visit our comprehensive documentation site →](https://openiap.dev/docs/setup/flutter)** ## 📦 Installation -```yaml -dependencies: - flutter_inapp_purchase: ^8.0.0 +```bash +flutter pub add flutter_inapp_purchase ``` +For manual `pubspec.yaml` edits, copy the current dependency from the +[flutter_inapp_purchase pub.dev package page](https://pub.dev/packages/flutter_inapp_purchase). + ## 🔧 Quick Start ### Basic Usage @@ -56,11 +58,11 @@ await iap.requestPurchaseWithBuilder( flutter_inapp_purchase provides AI-friendly documentation for Cursor, GitHub Copilot, Claude, and ChatGPT. -**[AI Assistants Guide](https://hyochan.github.io/flutter_inapp_purchase/docs/guides/ai-assistants)** +**[AI Assistants Guide](https://openiap.dev/docs/guides/ai-assistants)** Quick links: -- [llms.txt](https://hyochan.github.io/flutter_inapp_purchase/llms.txt) - Quick reference -- [llms-full.txt](https://hyochan.github.io/flutter_inapp_purchase/llms-full.txt) - Full API reference +- [llms.txt](https://openiap.dev/llms.txt) - Quick reference +- [llms-full.txt](https://openiap.dev/llms-full.txt) - Full API reference ## Development diff --git a/libraries/flutter_inapp_purchase/ancillary/amazon.sdktester.json b/libraries/flutter_inapp_purchase/ancillary/amazon.sdktester.json deleted file mode 100644 index 265e7004..00000000 --- a/libraries/flutter_inapp_purchase/ancillary/amazon.sdktester.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "android.test.purchased" : { - "itemType" : "ENTITLED", - "price" : "4.99", - "title": "Test Single Purchase", - "description": "One purchase only", - "smallIconUrl": "./assets/sample.theme.red.jpg" - }, - "android.test.canceled" : { - "itemType" : "ENTITLED", - "price" : "0.99", - "title": "Test Single Purchase", - "description": "One purchase only", - "smallIconUrl": "./assets/sample.theme.green.jpg" - }, - "point_1000" : { - "itemType" : "SUBSCRIPTION", - "price" : "0.99", - "title": "1000 Points", - "description": "1000 Points to spend", - "smallIconUrl": "./assets/sample.theme.blue.jpg" - }, - "5000_point" : { - "itemType": "CONSUMABLE", - "price": "10.00", - "title": "Orange", - "description": "An orange", - "smallIconUrl": "http://www.amazon.com/orange.jpg" - } -} diff --git a/libraries/flutter_inapp_purchase/android/build.gradle b/libraries/flutter_inapp_purchase/android/build.gradle index fe0dc3ac..04592ef5 100644 --- a/libraries/flutter_inapp_purchase/android/build.gradle +++ b/libraries/flutter_inapp_purchase/android/build.gradle @@ -1,4 +1,5 @@ import groovy.json.JsonSlurper +import org.jetbrains.kotlin.gradle.dsl.JvmTarget static File locateOpeniapVersionsFile(File pluginDir, File hostProjectRoot) { // First try to find in host app's root directory @@ -20,22 +21,89 @@ static File locateOpeniapVersionsFile(File pluginDir, File hostProjectRoot) { ) } +static String readPubspecVersion(File pubspecFile) { + if (!pubspecFile.isFile()) { + throw new GradleException("flutter_inapp_purchase: Unable to locate pubspec.yaml") + } + def matcher = pubspecFile.text =~ /(?m)^version:\s*([^\s#]+)/ + if (!matcher.find()) { + throw new GradleException("flutter_inapp_purchase: 'version' missing in pubspec.yaml") + } + return matcher.group(1).trim() +} + +static String readRequiredAndroidGradleProperty(File androidDir, String propertyName) { + File propertiesFile = new File(androidDir, 'gradle.properties') + if (!propertiesFile.isFile()) { + throw new GradleException("flutter_inapp_purchase: missing android/gradle.properties") + } + + Properties properties = new Properties() + propertiesFile.withInputStream { properties.load(it) } + String value = properties.getProperty(propertyName) + if (value == null || value.trim().isEmpty()) { + throw new GradleException("flutter_inapp_purchase: missing ${propertyName} in android/gradle.properties") + } + return value.trim() +} + def openiapVersionsFile = locateOpeniapVersionsFile(buildscript.sourceFile.parentFile.parentFile, project.rootProject.rootDir) def openiapVersions = new JsonSlurper().parse(openiapVersionsFile) def openiapGoogleVersion = openiapVersions['google'] +if (!(openiapGoogleVersion instanceof String) || !openiapGoogleVersion.trim()) { + throw new GradleException("flutter_inapp_purchase: 'google' version missing or invalid in openiap-versions.json") +} +openiapGoogleVersion = openiapGoogleVersion.trim() +def flutterPackageVersion = readPubspecVersion(new File(projectDir.parentFile, 'pubspec.yaml')) -group 'io.github.hyochan.flutter_inapp_purchase' -version '1.0-SNAPSHOT' +group = 'io.github.hyochan.flutter_inapp_purchase' +version = flutterPackageVersion buildscript { - ext.kotlin_version = '2.0.21' + def locateGoogleRootBuildFile = { File startDir -> + File current = startDir + while (current != null) { + File candidate = new File(current, 'packages/google/build.gradle.kts') + if (candidate.isFile()) { + return candidate + } + current = current.parentFile + } + return null + } + + def readGradleProperty = { String propertyName -> + File propertiesFile = new File(projectDir, 'gradle.properties') + if (!propertiesFile.isFile()) { + return null + } + Properties properties = new Properties() + propertiesFile.withInputStream { properties.load(it) } + return properties.getProperty(propertyName) + } + + def googlePluginVersion = { String pluginId -> + File googleRootBuildFile = locateGoogleRootBuildFile(projectDir) + if (googleRootBuildFile == null) { + return null + } + String escapedPluginId = java.util.regex.Pattern.quote(pluginId) + def matcher = googleRootBuildFile.text =~ /id\("${escapedPluginId}"\) version "([^"]+)"/ + return matcher.find() ? matcher.group(1) : null + } + + def androidGradlePluginVersion = googlePluginVersion('com.android.library') + ?: readRequiredAndroidGradleProperty(projectDir, 'openIapAndroidGradlePluginVersion') + ext.kotlin_version = googlePluginVersion('org.jetbrains.kotlin.android') + ?: readRequiredAndroidGradleProperty(projectDir, 'openIapKotlinVersion') + repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.4' + classpath "com.android.tools.build:gradle:$androidGradlePluginVersion" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -50,18 +118,25 @@ rootProject.allprojects { apply plugin: 'com.android.library' apply plugin: 'kotlin-android' +apply from: project.file('openiap-android-sdk.gradle') + +def openIapCompileSdkVersion = openIapResolveAndroidSdkVersion('compileSdkVersion', 'compileSdk', 35) +def openIapMinSdkVersion = openIapResolveAndroidSdkVersion('minSdkVersion', 'minSdk', 23) +def openIapTargetSdkVersion = openIapResolveAndroidSdkVersion('targetSdkVersion', 'compileSdk', 35) + android { if (project.android.hasProperty('namespace')) { - namespace 'io.github.hyochan.flutter_inapp_purchase' + namespace = 'io.github.hyochan.flutter_inapp_purchase' } - compileSdkVersion 34 + compileSdk = openIapCompileSdkVersion // Read horizonEnabled from gradle.properties, default to false (play) def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false defaultConfig { - minSdkVersion 21 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + minSdkVersion = openIapMinSdkVersion + targetSdkVersion = openIapTargetSdkVersion + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" // Use horizonEnabled to determine platform flavor def flavor = horizonEnabled ? 'horizon' : 'play' @@ -78,10 +153,6 @@ android { targetCompatibility JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = '17' - } - flavorDimensions "platform" productFlavors { // Play flavor - Google Play Billing (default) @@ -96,6 +167,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + dependencies { // In monorepo: use local packages/google source if available def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false @@ -108,11 +185,8 @@ dependencies { implementation "io.github.hyochan.openiap:openiap-google:${openiapGoogleVersion}" } - // Amazon IAP for legacy Amazon Appstore support (runtime device detection) - implementation files('jars/in-app-purchasing-2.0.76.jar') - implementation 'androidx.annotation:annotation:1.6.0' + implementation "androidx.annotation:annotation:${readRequiredAndroidGradleProperty(projectDir, 'openIapAndroidAnnotationVersion')}" - // Google Play Billing for direct API usage (only in play flavor) - // Note: This is already included in openiap, but kept for backward compatibility - add("playCompileOnly", "com.android.billingclient:billing-ktx:8.0.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/gradle.properties b/libraries/flutter_inapp_purchase/android/gradle.properties index 53ae0ae4..e0b62405 100644 --- a/libraries/flutter_inapp_purchase/android/gradle.properties +++ b/libraries/flutter_inapp_purchase/android/gradle.properties @@ -1,3 +1,7 @@ android.enableJetifier=true android.useAndroidX=true org.gradle.jvmargs=-Xmx1536M +openIapAndroidGradlePluginVersion=8.13.2 +openIapKotlinVersion=2.2.0 +openIapAndroidAnnotationVersion=1.6.0 +openIapJunitVersion=4.13.2 diff --git a/libraries/flutter_inapp_purchase/android/gradle/wrapper/gradle-wrapper.properties b/libraries/flutter_inapp_purchase/android/gradle/wrapper/gradle-wrapper.properties index bdc9a83b..c6f00302 100644 --- a/libraries/flutter_inapp_purchase/android/gradle/wrapper/gradle-wrapper.properties +++ b/libraries/flutter_inapp_purchase/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/libraries/flutter_inapp_purchase/android/jars/in-app-purchasing-2.0.76.jar b/libraries/flutter_inapp_purchase/android/jars/in-app-purchasing-2.0.76.jar deleted file mode 100644 index cd590eb9..00000000 Binary files a/libraries/flutter_inapp_purchase/android/jars/in-app-purchasing-2.0.76.jar and /dev/null differ diff --git a/libraries/flutter_inapp_purchase/android/openiap-android-sdk.gradle b/libraries/flutter_inapp_purchase/android/openiap-android-sdk.gradle new file mode 100644 index 00000000..fda283cc --- /dev/null +++ b/libraries/flutter_inapp_purchase/android/openiap-android-sdk.gradle @@ -0,0 +1,60 @@ +ext.openIapFindGoogleOpenIapBuildFile = { + File current = projectDir + while (current != null) { + File candidate = new File(current, 'packages/google/openiap/build.gradle.kts') + if (candidate.isFile()) { + return candidate + } + current = current.parentFile + } + return null +} + +ext.openIapReadGoogleAndroidSdkVersion = { String propertyName -> + File buildFile = openIapFindGoogleOpenIapBuildFile() + if (buildFile == null) { + return null + } + def matcher = buildFile.text =~ /(?m)^\s*${propertyName}\s*=\s*(\d+).*$/ + return matcher.find() ? matcher.group(1).toInteger() : null +} + +ext.openIapReadGoogleDependencyVersion = { String coordinate -> + File buildFile = openIapFindGoogleOpenIapBuildFile() + if (buildFile == null) { + return null + } + def matcher = buildFile.text =~ /${java.util.regex.Pattern.quote(coordinate)}:([^"$)]+)/ + return matcher.find() ? matcher.group(1) : null +} + +ext.openIapToIntegerVersion = { Object value, String label -> + if (value instanceof Number) { + return value.toInteger() + } + if (value instanceof CharSequence && value.toString() ==~ /\d+/) { + return value.toString().toInteger() + } + throw new GradleException("flutter_inapp_purchase: ${label} must be an integer, got ${value}") +} + +ext.openIapResolveAndroidSdkVersion = { String extName, String googlePropertyName, int fallback -> + if (rootProject.ext.has(extName)) { + return openIapToIntegerVersion(rootProject.ext.get(extName), extName) + } + def googleValue = openIapReadGoogleAndroidSdkVersion(googlePropertyName) + return googleValue ?: fallback +} + +ext.openIapResolveDependencyVersion = { String coordinate, String fallbackPropertyName -> + def googleValue = openIapReadGoogleDependencyVersion(coordinate) + if (googleValue) { + return googleValue + } + + def fallbackValue = rootProject.findProperty(fallbackPropertyName) ?: project.findProperty(fallbackPropertyName) + if (fallbackValue == null || fallbackValue.toString().trim().isEmpty()) { + throw new GradleException("flutter_inapp_purchase: missing ${fallbackPropertyName} in android/gradle.properties") + } + return fallbackValue.toString().trim() +} diff --git a/libraries/flutter_inapp_purchase/android/settings.gradle b/libraries/flutter_inapp_purchase/android/settings.gradle index 8884e4ba..961a10e4 100644 --- a/libraries/flutter_inapp_purchase/android/settings.gradle +++ b/libraries/flutter_inapp_purchase/android/settings.gradle @@ -1,11 +1,7 @@ rootProject.name = 'flutter_inapp_purchase' -// Optional: include local openiap-google module for debugging -// 1) git clone https://github.com/hyodotdev/openiap-google -// 2) Update path below to your local checkout -// 3) In android/build.gradle, switch dependency to: -// implementation project(":openiap") -// (and comment out the Maven Central dependency) +// 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('/path/to/openiap/packages/google/openiap') +// project(':openiap').projectDir = new File(settingsDir, '../../../packages/google/openiap') diff --git a/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AmazonInappPurchasePlugin.kt b/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AmazonInappPurchasePlugin.kt deleted file mode 100644 index 21e027c2..00000000 --- a/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AmazonInappPurchasePlugin.kt +++ /dev/null @@ -1,271 +0,0 @@ -package io.github.hyochan.flutter_inapp_purchase - -import android.app.Activity -import android.content.Context -import android.util.Log -import com.amazon.device.iap.PurchasingListener -import com.amazon.device.iap.PurchasingService -import com.amazon.device.iap.model.* -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import org.json.JSONArray -import org.json.JSONException -import org.json.JSONObject - -/** AmazonInappPurchasePlugin */ -class AmazonInappPurchasePlugin : MethodCallHandler { - private val TAG = "InappPurchasePlugin" - private var safeResult: MethodResultWrapper? = null - private var channel: MethodChannel? = null - private var context: Context? = null - private var activity: Activity? = null - fun setContext(context: Context?) { - this.context = context - } - - fun setActivity(activity: Activity?) { - this.activity = activity - } - - fun setChannel(channel: MethodChannel?) { - this.channel = channel - } - - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - if(call.method == "getStore"){ - result.success(FlutterInappPurchasePlugin.getStore()) - return - } - - val ch = channel - if (ch == null) { - Log.e(TAG, "onMethodCall received for ${call.method} but channel is null. Cannot send result.") - result.error("E_CHANNEL_NULL", "MethodChannel is not attached", null) - return - } - safeResult = MethodResultWrapper(result, ch) - - try { - PurchasingService.registerListener(context, purchasesUpdatedListener) - } catch (e: Exception) { - safeResult!!.error( - call.method, - "Call endConnection method if you want to start over.", - e.message - ) - } - when (call.method) { - "initConnection" -> { - PurchasingService.getUserData() - safeResult!!.success("Billing client ready") - } - "endConnection" -> { - safeResult!!.success("Billing client has ended.") - } - "isReady" -> { - safeResult!!.success(true) - } - "showInAppMessages" -> { - safeResult!!.success("in app messages not supported for amazon") - } - "getAvailableItemsByType" -> { - val type = call.argument("type") - if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "gaibt=$type") - // NOTE: getPurchaseUpdates doesnt return Consumables which are FULFILLED - if (type == "inapp") { - PurchasingService.getPurchaseUpdates(true) - } else if (type == "subs") { - // Subscriptions are retrieved during inapp, so we just return empty list - safeResult!!.success("[]") - } else { - safeResult!!.notImplemented() - } - } - "getPurchaseHistoryByType" -> { - // No equivalent - safeResult!!.success("[]") - } - "buyItemByType" -> { - val type = call.argument("type") - //val obfuscatedAccountId = call.argument("obfuscatedAccountId") - //val obfuscatedProfileId = call.argument("obfuscatedProfileId") - val sku = call.argument("sku") - val oldSku = call.argument("oldSku") - // TODO(v6.4.0): Remove this commented prorationMode line - //val prorationMode = call.argument("prorationMode")!! - if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "type=$type||sku=$sku||oldsku=$oldSku") - val requestId = PurchasingService.purchase(sku) - if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "resid=$requestId") - } - "consumeProduct" -> { - // consumable is a separate type in amazon - safeResult!!.success("no-ops in amazon") - } - else -> { - safeResult!!.notImplemented() - } - } - } - - private val purchasesUpdatedListener: PurchasingListener = object : PurchasingListener { - override fun onUserDataResponse(userDataResponse: UserDataResponse) { - if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "oudr=$userDataResponse") - } - - // getItemsByType - override fun onProductDataResponse(response: ProductDataResponse) { - if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "opdr=$response") - val status = response.requestStatus - if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "onProductDataResponse: RequestStatus ($status)") - when (status) { - ProductDataResponse.RequestStatus.SUCCESSFUL -> { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d( - TAG, - "onProductDataResponse: successful. The item data map in this response includes the valid SKUs" - ) - } - val productData = response.productData - //Log.d(TAG, "productData="+productData.toString()); - val unavailableSkus = response.unavailableSkus - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d( - TAG, - "onProductDataResponse: " + unavailableSkus.size + " unavailable skus" - ) - Log.d(TAG, "unavailableSkus=$unavailableSkus") - } - val items = JSONArray() - try { - for ((_, product) in productData) { - //val format = NumberFormat.getCurrencyInstance() - val item = JSONObject() - item.put("productId", product.sku) - item.put("price", product.price) - item.put("currency", null) - when (product.productType) { - ProductType.ENTITLED, ProductType.CONSUMABLE -> item.put( - "type", - "inapp" - ) - ProductType.SUBSCRIPTION -> item.put("type", "subs") - } - item.put("localizedPrice", product.price) - item.put("title", product.title) - item.put("description", product.description) - item.put("introductoryPrice", "") - item.put("subscriptionPeriodAndroid", "") - item.put("freeTrialPeriodAndroid", "") - item.put("introductoryPriceCyclesAndroid", 0) - item.put("introductoryPricePeriodAndroid", "") - if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "opdr Putting $item") - items.put(item) - } - //System.err.println("Sending "+items.toString()); - safeResult!!.success(items.toString()) - } catch (e: JSONException) { - safeResult!!.error(TAG, "E_BILLING_RESPONSE_JSON_PARSE_ERROR", e.message) - } - } - ProductDataResponse.RequestStatus.FAILED -> { - safeResult!!.error(TAG, "FAILED", null) - if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "onProductDataResponse: failed, should retry request") - safeResult!!.error(TAG, "NOT_SUPPORTED", null) - } - ProductDataResponse.RequestStatus.NOT_SUPPORTED -> { - if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "onProductDataResponse: failed, should retry request") - safeResult!!.error(TAG, "NOT_SUPPORTED", null) - } - } - } - - // buyItemByType - override fun onPurchaseResponse(response: PurchaseResponse) { - if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "opr=$response") - when (val status = response.requestStatus) { - PurchaseResponse.RequestStatus.SUCCESSFUL -> { - val receipt = response.receipt - PurchasingService.notifyFulfillment( - receipt.receiptId, - FulfillmentResult.FULFILLED - ) - val date = receipt.purchaseDate - val transactionDate = date.time - try { - val item = getPurchaseData( - receipt.sku, - receipt.receiptId, - receipt.receiptId, - transactionDate.toDouble() - ) - if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "opr Putting $item") - safeResult!!.success(item.toString()) - safeResult!!.invokeMethod("purchase-updated", item.toString()) - } catch (e: JSONException) { - safeResult!!.error(TAG, "E_BILLING_RESPONSE_JSON_PARSE_ERROR", e.message) - } - } - PurchaseResponse.RequestStatus.FAILED -> safeResult!!.error( - TAG, - "buyItemByType", - "billingResponse is not ok: $status" - ) - else -> {} - } - } - - // getAvailableItemsByType - override fun onPurchaseUpdatesResponse(response: PurchaseUpdatesResponse) { - if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "opudr=$response") - when (response.requestStatus) { - PurchaseUpdatesResponse.RequestStatus.SUCCESSFUL -> { - val items = JSONArray() - try { - val receipts = response.receipts - for (receipt in receipts) { - val date = receipt.purchaseDate - val transactionDate = date.time - val item = getPurchaseData( - receipt.sku, - receipt.receiptId, - receipt.receiptId, - transactionDate.toDouble() - ) - if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "opudr Putting $item") - items.put(item) - } - safeResult!!.success(items.toString()) - } catch (e: JSONException) { - safeResult!!.error(TAG, "E_BILLING_RESPONSE_JSON_PARSE_ERROR", e.message) - } - } - PurchaseUpdatesResponse.RequestStatus.FAILED -> safeResult!!.error( - TAG, - "FAILED", - null - ) - PurchaseUpdatesResponse.RequestStatus.NOT_SUPPORTED -> { - if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "onPurchaseUpdatesResponse: failed, should retry request") - safeResult!!.error(TAG, "NOT_SUPPORTED", null) - } - } - } - } - - @Throws(JSONException::class) - fun getPurchaseData( - productId: String?, transactionId: String?, transactionReceipt: String?, - transactionDate: Double? - ): JSONObject { - val item = JSONObject() - item.put("productId", productId) - item.put("transactionId", transactionId) - item.put("transactionReceipt", transactionReceipt) - item.put("transactionDate", (transactionDate!!).toString()) - item.put("dataAndroid", null) - item.put("signatureAndroid", null) - item.put("purchaseToken", null) - return item - } -} 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 4926bd36..1c587ab4 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 @@ -5,6 +5,8 @@ import android.app.Application import android.app.Application.ActivityLifecycleCallbacks import android.content.Context import android.os.Bundle +import android.os.Handler +import android.os.Looper import dev.hyo.openiap.AndroidSubscriptionOfferInput import dev.hyo.openiap.BillingProgramAndroid import dev.hyo.openiap.DeepLinkOptions @@ -51,6 +53,7 @@ import java.util.Locale class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, ActivityLifecycleCallbacks { private val job = Job() private val scope = CoroutineScope(Dispatchers.Main + job) + private val handler = Handler(Looper.getMainLooper()) private var context: Context? = null private var activity: Activity? = null @@ -187,6 +190,11 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act this.error(code, resolvedMessage, null) } + private fun emitConnectionUpdated(connected: Boolean) { + val item = JSONObject().apply { put("connected", connected) } + handler.post { channel?.invokeMethod("connection-updated", item.toString()) } + } + fun setContext(context: Context?) { this.context = context if (context != null && openIap == null) { @@ -204,12 +212,25 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act } fun onDetachedFromActivity() { + activity = null + openIap?.setActivity(null) + } + + fun dispose() { + val iap = openIap + openIap = null + context = null + activity = null + channel = null + connectionReady = false + listenersAttached = false scope.launch { - kotlin.runCatching { openIap?.endConnection() } - connectionReady = false + connectionMutex.withLock { + kotlin.runCatching { iap?.endConnection() } + } + }.invokeOnCompletion { + job.cancel() } - // Cancel coroutine job to avoid leaks - job.cancel() } // ActivityLifecycleCallbacks (no-ops except for cleanup) @@ -220,8 +241,8 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act override fun onActivityStopped(activity: Activity) {} override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} override fun onActivityDestroyed(activity: Activity) { - if (this.activity === activity && context != null) { - (context as Application).unregisterActivityLifecycleCallbacks(this) + if (this.activity === activity) { + (context as? Application)?.unregisterActivityLifecycleCallbacks(this) onDetachedFromActivity() } } @@ -246,7 +267,12 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act val packageName = call.argument("packageName") scope.launch { try { - openIap?.deepLinkToSubscriptions(DeepLinkOptions(skuAndroid = sku, packageNameAndroid = packageName)) + val iap = openIap + if (iap == null) { + safe.error(OpenIapError.NotPrepared.CODE, OpenIapError.NotPrepared.MESSAGE, "IAP module not initialized.") + return@launch + } + iap.deepLinkToSubscriptions(DeepLinkOptions(skuAndroid = sku, packageNameAndroid = packageName)) safe.success(true) } catch (e: Exception) { safe.error(OpenIapError.BillingError.CODE, OpenIapError.BillingError.MESSAGE, e.message) @@ -257,7 +283,12 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act "openPlayStoreSubscriptions" -> { scope.launch { try { - openIap?.deepLinkToSubscriptions(DeepLinkOptions()) + val iap = openIap + if (iap == null) { + safe.error(OpenIapError.NotPrepared.CODE, OpenIapError.NotPrepared.MESSAGE, "IAP module not initialized.") + return@launch + } + iap.deepLinkToSubscriptions(DeepLinkOptions()) safe.success(true) } catch (e: Exception) { safe.error(OpenIapError.BillingError.CODE, OpenIapError.BillingError.MESSAGE, e.message) @@ -287,63 +318,71 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act OpenIapLog.d(TAG, "initConnection called with config: $configMap") - attachListenersIfNeeded() - openIap?.setActivity(activity) scope.launch { - try { - // ALWAYS end connection first to reset configuration - // This ensures we start fresh regardless of current state + connectionMutex.withLock { try { - OpenIapLog.d(TAG, "Ending connection before reinitializing (current ready state: $connectionReady)") - openIap?.endConnection() - connectionReady = false + attachListenersIfNeeded() + openIap?.setActivity(activity) - // WORKAROUND: OpenIAP's endConnection() is synchronous but may trigger - // async cleanup in the background (e.g., disconnecting from Play Store). - // A small delay reduces the risk of race conditions where initConnection() - // is called before cleanup completes. This is not ideal but necessary - // until OpenIAP provides an async endConnection() or callback mechanism. - // Increase this delay if experiencing connection issues. - kotlinx.coroutines.delay(300) - } catch (e: Exception) { - OpenIapLog.w(TAG, "Error ending connection: ${e.message}") - } + // ALWAYS end connection first to reset configuration + // This ensures we start fresh regardless of current state + try { + OpenIapLog.d(TAG, "Ending connection before reinitializing (current ready state: $connectionReady)") + openIap?.endConnection() + connectionReady = false + + // WORKAROUND: OpenIAP's endConnection() is synchronous but may trigger + // async cleanup in the background (e.g., disconnecting from Play Store). + // A small delay reduces the risk of race conditions where initConnection() + // is called before cleanup completes. This is not ideal but necessary + // until OpenIAP provides an async endConnection() or callback mechanism. + // Increase this delay if experiencing connection issues. + kotlinx.coroutines.delay(300) + } catch (e: Exception) { + OpenIapLog.w(TAG, "Error ending connection: ${e.message}") + } - OpenIapLog.d(TAG, "Initializing connection with Alternative Billing mode: ${configMap.get("alternativeBillingModeAndroid") ?: "none"}") - val ok = openIap?.initConnection(newConfig) ?: false - connectionReady = ok - OpenIapLog.d(TAG, "Connection initialized: $ok") + OpenIapLog.d(TAG, "Initializing connection with Alternative Billing mode: ${configMap.get("alternativeBillingModeAndroid") ?: "none"}") + val ok = openIap?.initConnection(newConfig) ?: false + connectionReady = ok + OpenIapLog.d(TAG, "Connection initialized: $ok") - // Emit connection-updated for compatibility - val item = JSONObject().apply { put("connected", ok) } - channel?.invokeMethod("connection-updated", item.toString()) - if (ok) { - safe.success("Billing client ready") - } else { - safe.error(OpenIapError.InitConnection.CODE, OpenIapError.InitConnection.MESSAGE, "responseCode: -1") + // Emit connection-updated for compatibility + emitConnectionUpdated(ok) + if (ok) { + safe.success("Billing client ready") + } else { + safe.error(OpenIapError.InitConnection.CODE, OpenIapError.InitConnection.MESSAGE, "responseCode: -1") + } + } catch (e: Exception) { + OpenIapLog.e("Error during initConnection: ${e.message}", e) + safe.error(OpenIapError.InitConnection.CODE, OpenIapError.InitConnection.MESSAGE, e.message) } - } catch (e: Exception) { - OpenIapLog.e("Error during initConnection: ${e.message}", e) - safe.error(OpenIapError.InitConnection.CODE, OpenIapError.InitConnection.MESSAGE, e.message) } } return } "endConnection" -> { scope.launch { - try { - OpenIapLog.d(TAG, "endConnection called") - openIap?.endConnection() - connectionReady = false - OpenIapLog.d(TAG, "Connection ended successfully") - safe.success("Billing client has ended.") - } catch (e: Exception) { - OpenIapLog.e("Error ending connection: ${e.message}", e) - safe.error(OpenIapError.BillingError.CODE, OpenIapError.BillingError.MESSAGE, e.message) + connectionMutex.withLock { + try { + OpenIapLog.d(TAG, "endConnection called") + openIap?.endConnection() + connectionReady = false + OpenIapLog.d(TAG, "Connection ended successfully") + safe.success("Billing client has ended.") + } catch (e: Exception) { + OpenIapLog.e("Error ending connection: ${e.message}", e) + safe.error(OpenIapError.BillingError.CODE, OpenIapError.BillingError.MESSAGE, e.message) + } } } return } + "setPurchaseUpdatedListenerOptions" -> { + safe.success(null) + return + } "isReady" -> { safe.success(connectionReady) return @@ -623,8 +662,10 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act } } "acknowledgePurchaseAndroid" -> { - val token = call.argument("token") ?: call.argument("purchaseToken") - if (token.isNullOrBlank()) { + val token: String? = call.argument("token") + ?: call.argument("purchaseToken") + val purchaseToken = token?.takeIf { it.isNotBlank() } + if (purchaseToken == null) { safe.error(OpenIapError.DeveloperError.CODE, OpenIapError.DeveloperError.MESSAGE, "Missing purchaseToken") return } @@ -635,7 +676,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act safe.error(OpenIapError.NotPrepared.CODE, OpenIapError.NotPrepared.MESSAGE, "IAP module not initialized.") return@launch } - iap.acknowledgePurchaseAndroid(token) + iap.acknowledgePurchaseAndroid(purchaseToken) val resp = JSONObject().apply { put("responseCode", 0) } safe.success(resp.toString()) } catch (e: Exception) { @@ -643,10 +684,11 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act } } } - "consumePurchaseAndroid" -> { - val token = call.argument("token") ?: call.argument("purchaseToken") - if (token.isNullOrBlank()) { + val token: String? = call.argument("token") + ?: call.argument("purchaseToken") + val purchaseToken = token?.takeIf { it.isNotBlank() } + if (purchaseToken == null) { safe.error(OpenIapError.DeveloperError.CODE, OpenIapError.DeveloperError.MESSAGE, "Missing purchaseToken") return } @@ -657,10 +699,10 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act safe.error(OpenIapError.NotPrepared.CODE, OpenIapError.NotPrepared.MESSAGE, "IAP module not initialized.") return@launch } - iap.consumePurchaseAndroid(token) + iap.consumePurchaseAndroid(purchaseToken) val resp = JSONObject().apply { put("responseCode", 0) - put("purchaseToken", token) + put("purchaseToken", purchaseToken) } safe.success(resp.toString()) } catch (e: Exception) { @@ -678,6 +720,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act safe.error(OpenIapError.NotPrepared.CODE, OpenIapError.NotPrepared.MESSAGE, "IAP module not initialized.") return@launch } + @Suppress("DEPRECATION") val isAvailable = iap.checkAlternativeBillingAvailability() safe.success(isAvailable) } catch (e: Exception) { @@ -698,6 +741,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act safe.error(OpenIapError.BillingError.CODE, OpenIapError.BillingError.MESSAGE, "Activity not available") return@launch } + @Suppress("DEPRECATION") val userAccepted = iap.showAlternativeBillingInformationDialog(act) safe.success(userAccepted) } catch (e: Exception) { @@ -713,6 +757,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act safe.error(OpenIapError.NotPrepared.CODE, OpenIapError.NotPrepared.MESSAGE, "IAP module not initialized.") return@launch } + @Suppress("DEPRECATION") val token = iap.createAlternativeBillingReportingToken() safe.success(token) } catch (e: Exception) { @@ -768,7 +813,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act val programStr = call.argument("billingProgram") val launchModeStr = call.argument("launchMode") val linkTypeStr = call.argument("linkType") - val linkUri = call.argument("linkUri") + val linkUri: String? = call.argument("linkUri")?.takeIf { it.isNotBlank() } scope.launch { try { @@ -782,7 +827,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act safe.error(OpenIapError.BillingError.CODE, OpenIapError.BillingError.MESSAGE, "Activity not available") return@launch } - if (linkUri.isNullOrBlank()) { + if (linkUri == null) { safe.error(OpenIapError.DeveloperError.CODE, "linkUri is required for launchExternalLinkAndroid", null) return@launch } @@ -803,8 +848,6 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act // Legacy/compat purchases queries "getAvailableItemsByType" -> { logDeprecated("getAvailableItemsByType", "Use getAvailableItems() instead") - val typeStr = call.argument("type") ?: "inapp" - val reqType = parsePurchaseType(typeStr) scope.launch { // Ensure connection for legacy path connectionMutex.withLock { @@ -814,8 +857,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act if (!connectionReady) { val ok = openIap?.initConnection(InitConnectionConfig()) ?: false connectionReady = ok - val item = JSONObject().apply { put("connected", ok) } - channel?.invokeMethod("connection-updated", item.toString()) + emitConnectionUpdated(ok) if (!ok) { safe.error(OpenIapError.InitConnection.CODE, OpenIapError.InitConnection.MESSAGE, "Failed to initialize connection") return@launch @@ -842,8 +884,6 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act } "getPurchaseHistoryByType" -> { logDeprecated("getPurchaseHistoryByType", "Use getAvailableItems() instead") - val typeStr = call.argument("type") ?: "inapp" - val reqType = parsePurchaseType(typeStr) scope.launch { // Ensure connection for legacy path connectionMutex.withLock { @@ -853,8 +893,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act if (!connectionReady) { val ok = openIap?.initConnection(InitConnectionConfig()) ?: false connectionReady = ok - val item = JSONObject().apply { put("connected", ok) } - channel?.invokeMethod("connection-updated", item.toString()) + emitConnectionUpdated(ok) if (!ok) { safe.error(OpenIapError.InitConnection.CODE, OpenIapError.InitConnection.MESSAGE, "Failed to initialize connection") return@launch @@ -873,8 +912,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act } // Note: As of v6.4.6+, getAvailablePurchases returns only active purchases on Android. // Purchase history (including expired/consumed items) is not supported on Android - // by the OpenIAP library. The reqType parameter is preserved for backward compatibility - // but is not used. Apps should migrate to getAvailableItems() for active purchases. + // by the OpenIAP library. Apps should migrate to getAvailableItems() for active purchases. val purchases = iap.getAvailablePurchases(null) val arr = purchasesToJsonArray(purchases) safe.success(arr.toString()) @@ -888,14 +926,15 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act "buyItemByType" -> { logDeprecated("buyItemByType", "Use requestPurchase(params) instead") val typeStr = call.argument("type") - val productId = call.argument("productId") + val productId: String? = call.argument("productId") ?: call.argument("sku") ?: call.argument>("skus")?.firstOrNull() val obfuscatedAccountId = call.argument("obfuscatedAccountId") val obfuscatedProfileId = call.argument("obfuscatedProfileId") val isOfferPersonalized = call.argument("isOfferPersonalized") ?: false - if (productId.isNullOrBlank()) { + val requestedProductId = productId?.takeIf { it.isNotBlank() } + if (requestedProductId == null) { safe.error(OpenIapError.DeveloperError.CODE, OpenIapError.DeveloperError.MESSAGE, "Missing productId") return } @@ -909,16 +948,15 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act if (!connectionReady) { val ok = openIap?.initConnection(InitConnectionConfig()) ?: false connectionReady = ok - val item = JSONObject().apply { put("connected", ok) } - channel?.invokeMethod("connection-updated", item.toString()) + emitConnectionUpdated(ok) if (!ok) { safe.error(OpenIapError.InitConnection.CODE, OpenIapError.InitConnection.MESSAGE, "Failed to initialize connection") - return@withLock + return@launch } } } catch (e: Exception) { safe.error(OpenIapError.BillingError.CODE, OpenIapError.BillingError.MESSAGE, e.message) - return@withLock + return@launch } } try { @@ -927,7 +965,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act safe.error(OpenIapError.NotPrepared.CODE, OpenIapError.NotPrepared.MESSAGE, "IAP module not initialized.") return@launch } - val skus = listOf(productId) + val skus: List = listOf(requestedProductId) val purchaseType = parsePurchaseType(typeStr) val requestProps = buildRequestPurchaseProps( type = purchaseType, @@ -951,8 +989,9 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act // Finish/acknowledge/consume (compat) "acknowledgePurchase" -> { logDeprecated("acknowledgePurchase", "Use acknowledgePurchaseAndroid(token) instead") - val token = call.argument("purchaseToken") - if (token.isNullOrBlank()) { + val token: String? = call.argument("purchaseToken") + val purchaseToken = token?.takeIf { it.isNotBlank() } + if (purchaseToken == null) { safe.error(OpenIapError.DeveloperError.CODE, OpenIapError.DeveloperError.MESSAGE, "Missing purchaseToken") return } @@ -963,7 +1002,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act safe.error(OpenIapError.NotPrepared.CODE, OpenIapError.NotPrepared.MESSAGE, "IAP module not initialized.") return@launch } - iap.acknowledgePurchaseAndroid(token) + iap.acknowledgePurchaseAndroid(purchaseToken) val resp = JSONObject().apply { put("responseCode", 0) } safe.success(resp.toString()) } catch (e: Exception) { @@ -971,12 +1010,12 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act } } } - "consumeProduct" -> { logDeprecated("consumeProduct", "Use finishTransaction(purchase, isConsumable=true) at higher-level API") - val token = call.argument("purchaseToken") - if (token.isNullOrBlank()) { + val token: String? = call.argument("purchaseToken") + val purchaseToken = token?.takeIf { it.isNotBlank() } + if (purchaseToken == null) { safe.error(OpenIapError.DeveloperError.CODE, OpenIapError.DeveloperError.MESSAGE, "Missing purchaseToken") return } @@ -987,10 +1026,10 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act safe.error(OpenIapError.NotPrepared.CODE, OpenIapError.NotPrepared.MESSAGE, "IAP module not initialized.") return@launch } - iap.consumePurchaseAndroid(token) + iap.consumePurchaseAndroid(purchaseToken) val resp = JSONObject().apply { put("responseCode", 0) - put("purchaseToken", token) + put("purchaseToken", purchaseToken) } safe.success(resp.toString()) } catch (e: Exception) { @@ -1000,8 +1039,9 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act } "consumePurchase" -> { logDeprecated("consumePurchase", "Use finishTransaction(purchase, isConsumable=true) at higher-level API") - val token = call.argument("purchaseToken") - if (token.isNullOrBlank()) { + val token: String? = call.argument("purchaseToken") + val purchaseToken = token?.takeIf { it.isNotBlank() } + if (purchaseToken == null) { safe.error(OpenIapError.DeveloperError.CODE, OpenIapError.DeveloperError.MESSAGE, "Missing purchaseToken") return } @@ -1012,7 +1052,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act safe.success(false) return@launch } - iap.consumePurchaseAndroid(token) + iap.consumePurchaseAndroid(purchaseToken) safe.success(true) } catch (e: Exception) { safe.success(false) @@ -1021,9 +1061,9 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act } - // No-op legacy (Deprecated — will be removed in 7.0.0) + // No-op legacy endpoint kept for compatibility until the next major cleanup. "showInAppMessages" -> { - logDeprecated("showInAppMessages", "No-op; removed in 7.0.0") + logDeprecated("showInAppMessages", "No-op legacy endpoint.") safe.success(true) } @@ -1189,9 +1229,8 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act } } - @Deprecated("Deprecated channel endpoint; will be removed in 7.0.0") private fun logDeprecated(name: String, message: String) { - OpenIapLog.w(TAG, "[$name] is deprecated and will be removed in 7.0.0. $message") + OpenIapLog.w(TAG, "[$name] is deprecated and will be removed in a future major version. $message") } /** @@ -1213,8 +1252,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act if (autoInit) { val ok = openIap?.initConnection(InitConnectionConfig()) ?: false connectionReady = ok - val item = JSONObject().apply { put("connected", ok) } - channel?.invokeMethod("connection-updated", item.toString()) + emitConnectionUpdated(ok) if (!ok) { safe.error(OpenIapError.InitConnection.CODE, OpenIapError.InitConnection.MESSAGE, "Failed to initialize connection") return @@ -1234,8 +1272,8 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act private fun attachListenersIfNeeded() { if (listenersAttached) return - listenersAttached = true - openIap?.addPurchaseUpdateListener(OpenIapPurchaseUpdateListener { p -> + val iap = openIap ?: return + iap.addPurchaseUpdateListener(OpenIapPurchaseUpdateListener { p -> scope.launch { try { val payload = JSONObject(p.toJson()) @@ -1245,26 +1283,17 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act } } }) - openIap?.addPurchaseErrorListener(OpenIapPurchaseErrorListener { e -> + iap.addPurchaseErrorListener(OpenIapPurchaseErrorListener { e -> scope.launch { try { - val payload = when (e) { - is OpenIapError -> JSONObject(e.toJSON()) - else -> JSONObject( - mapOf( - "code" to OpenIapError.PurchaseFailed.CODE, - "message" to (e.message ?: "Purchase error"), - "platform" to "android" - ) - ) - } + val payload = JSONObject(e.toJSON()) channel?.invokeMethod("purchase-error", payload.toString()) } catch (ex: Exception) { OpenIapLog.e("Failed to send purchase-error", ex) } } }) - openIap?.addUserChoiceBillingListener { details -> + iap.addUserChoiceBillingListener { details -> scope.launch { try { val payload = JSONObject(details.toJson()) @@ -1274,7 +1303,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act } } } - openIap?.addDeveloperProvidedBillingListener(OpenIapDeveloperProvidedBillingListener { details -> + iap.addDeveloperProvidedBillingListener(OpenIapDeveloperProvidedBillingListener { details -> scope.launch { try { val payload = JSONObject(details.toJson()) @@ -1284,7 +1313,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act } } }) - openIap?.addSubscriptionBillingIssueListener( + iap.addSubscriptionBillingIssueListener( dev.hyo.openiap.listener.OpenIapSubscriptionBillingIssueListener { purchase -> scope.launch { try { @@ -1296,11 +1325,11 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act } } ) + listenersAttached = true } companion object { private const val TAG = "InappPurchasePlugin" - private const val PLAY_STORE_URL = "https://play.google.com/store/account/subscriptions" private const val KEY_REQUEST_SUBSCRIPTION = "requestSubscription" private const val KEY_REQUEST_PURCHASE = "requestPurchase" 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 633b302e..98cb0cfd 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 @@ -3,84 +3,45 @@ package io.github.hyochan.flutter_inapp_purchase import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import android.content.Context +import android.util.Log import io.flutter.plugin.common.MethodChannel import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding import io.flutter.plugin.common.BinaryMessenger import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import android.content.pm.PackageManager.NameNotFoundException /** FlutterInappPurchasePlugin */ class FlutterInappPurchasePlugin : FlutterPlugin, ActivityAware { private var androidInappPurchasePlugin: AndroidInappPurchasePlugin? = null - private var amazonInappPurchasePlugin: AmazonInappPurchasePlugin? = null private var channel: MethodChannel? = null override fun onAttachedToEngine(binding: FlutterPluginBinding) { onAttached(binding.applicationContext, binding.binaryMessenger) } private fun onAttached(context: Context, messenger: BinaryMessenger) { - isAndroid = isPackageInstalled(context, "com.android.vending") - isAmazon = isPackageInstalled(context, "com.amazon.venezia") - - // In the case of an amazon device which has been side loaded with the Google Play store, - // we should use the store the app was installed from. - if (isAmazon && isAndroid) { - if (isAppInstalledFrom(context, "amazon")) { - isAndroid = false - } else { - isAmazon = false - } - } - - // If neither Play Store nor Amazon is detected, default to Android (for Horizon and other stores) - // This allows openiap to handle different billing implementations via flavors - if (!isAndroid && !isAmazon) { - android.util.Log.i("FlutterInappPurchase", "No Play Store or Amazon detected - defaulting to Android plugin (supports Horizon and other stores)") - isAndroid = true - } - - channel = MethodChannel(messenger, "flutter_inapp") - if (isAndroid) { - android.util.Log.i("FlutterInappPurchase", "Initializing Android IAP plugin") - val plugin = AndroidInappPurchasePlugin() - plugin.setContext(context) - plugin.setChannel(channel) - androidInappPurchasePlugin = plugin - channel!!.setMethodCallHandler(plugin) - } else if (isAmazon) { - android.util.Log.i("FlutterInappPurchase", "Initializing Amazon IAP plugin") - amazonInappPurchasePlugin = AmazonInappPurchasePlugin() - amazonInappPurchasePlugin!!.setContext(context) - amazonInappPurchasePlugin!!.setChannel(channel) - channel!!.setMethodCallHandler(amazonInappPurchasePlugin) - } + val methodChannel = MethodChannel(messenger, "flutter_inapp") + channel = methodChannel + logInfo("Initializing Android IAP plugin") + val plugin = AndroidInappPurchasePlugin() + plugin.setContext(context) + plugin.setChannel(methodChannel) + androidInappPurchasePlugin = plugin + methodChannel.setMethodCallHandler(plugin) } override fun onDetachedFromEngine(binding: FlutterPluginBinding) { - channel!!.setMethodCallHandler(null) + channel?.setMethodCallHandler(null) + androidInappPurchasePlugin?.dispose() + androidInappPurchasePlugin = null channel = null - if (isAndroid) { - androidInappPurchasePlugin?.setChannel(null) - } else if (isAmazon) { - amazonInappPurchasePlugin!!.setChannel(null) - } } override fun onAttachedToActivity(binding: ActivityPluginBinding) { - if (isAndroid) { - androidInappPurchasePlugin?.setActivity(binding.activity) - } else if (isAmazon) { - amazonInappPurchasePlugin!!.setActivity(binding.activity) - } + androidInappPurchasePlugin?.setActivity(binding.activity) } override fun onDetachedFromActivity() { - if (isAndroid) { - androidInappPurchasePlugin?.setActivity(null) - androidInappPurchasePlugin?.onDetachedFromActivity() - } else if (isAmazon) { - amazonInappPurchasePlugin!!.setActivity(null) - } + androidInappPurchasePlugin?.setActivity(null) + androidInappPurchasePlugin?.onDetachedFromActivity() } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { @@ -88,39 +49,20 @@ class FlutterInappPurchasePlugin : FlutterPlugin, ActivityAware { } override fun onDetachedFromActivityForConfigChanges() { - onDetachedFromActivity() - } - - private fun setAndroidInappPurchasePlugin(androidInappPurchasePlugin: AndroidInappPurchasePlugin) { - this.androidInappPurchasePlugin = androidInappPurchasePlugin - } - - private fun setAmazonInappPurchasePlugin(amazonInappPurchasePlugin: AmazonInappPurchasePlugin) { - this.amazonInappPurchasePlugin = amazonInappPurchasePlugin + androidInappPurchasePlugin?.setActivity(null) } companion object { - private var isAndroid = false - private var isAmazon = false + private const val TAG = "FlutterInappPurchase" - fun getStore(): String { - return if (!isAndroid && !isAmazon) "none" else if (isAndroid) "play_store" else "amazon" - } - - private fun isPackageInstalled(ctx: Context, packageName: String): Boolean { - return try { - ctx.packageManager.getPackageInfo(packageName, 0) - true - } catch (e: NameNotFoundException) { - false + private fun logInfo(message: String) { + if (Log.isLoggable(TAG, Log.INFO)) { + Log.i(TAG, message) } } - fun isAppInstalledFrom(ctx: Context, installer: String?): Boolean { - val installerPackageName = ctx.packageManager.getInstallerPackageName(ctx.packageName) - return installer != null && installerPackageName != null && installerPackageName.contains( - installer - ) + fun getStore(): String { + return "play_store" } } } diff --git a/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/MethodResultWrapper.kt b/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/MethodResultWrapper.kt index 5c788331..1c0ff717 100644 --- a/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/MethodResultWrapper.kt +++ b/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/MethodResultWrapper.kt @@ -1,8 +1,9 @@ package io.github.hyochan.flutter_inapp_purchase import android.os.Handler -import io.flutter.plugin.common.MethodChannel import android.os.Looper +import io.flutter.plugin.common.MethodChannel +import java.util.concurrent.atomic.AtomicBoolean // MethodChannel.Result wrapper that responds on the platform thread. class MethodResultWrapper internal constructor( @@ -10,33 +11,26 @@ class MethodResultWrapper internal constructor( private val safeChannel: MethodChannel ) : MethodChannel.Result { private val handler: Handler = Handler(Looper.getMainLooper()) - private var exhausted: Boolean = false + private val exhausted = AtomicBoolean(false) override fun success(result: Any?) { - if (!exhausted) { - exhausted = true - + if (exhausted.compareAndSet(false, true)) { handler.post { safeResult.success(result) } } } override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { - if (!exhausted) { - exhausted = true - + if (exhausted.compareAndSet(false, true)) { handler.post { safeResult.error(errorCode, errorMessage, errorDetails) } } } override fun notImplemented() { - if (!exhausted) { - exhausted = true - + if (exhausted.compareAndSet(false, true)) { handler.post { safeResult.notImplemented() } } } - fun invokeMethod(method: String?, arguments: Any?) { - handler.post { safeChannel.invokeMethod(method!!, arguments, null) } + fun invokeMethod(method: String, arguments: Any?) { + handler.post { safeChannel.invokeMethod(method, arguments, null) } } - -} \ No newline at end of file +} diff --git a/libraries/flutter_inapp_purchase/example/android/app/build.gradle b/libraries/flutter_inapp_purchase/example/android/app/build.gradle index d47fadb8..3e3c4168 100644 --- a/libraries/flutter_inapp_purchase/example/android/app/build.gradle +++ b/libraries/flutter_inapp_purchase/example/android/app/build.gradle @@ -1,9 +1,20 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id "com.android.application" id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" } +apply from: rootProject.file('../../android/openiap-android-sdk.gradle') + +def openIapCompileSdkVersion = openIapResolveAndroidSdkVersion('compileSdkVersion', 'compileSdk', 35) +def openIapMinSdkVersion = openIapResolveAndroidSdkVersion('minSdkVersion', 'minSdk', 23) +def openIapTargetSdkVersion = openIapResolveAndroidSdkVersion('targetSdkVersion', 'compileSdk', 35) +def openIapJunitVersion = openIapResolveDependencyVersion('junit:junit', 'openIapJunitVersion') +def openIapAndroidTestRunnerVersion = openIapResolveDependencyVersion('androidx.test:runner', 'openIapAndroidTestRunnerVersion') +def openIapEspressoCoreVersion = openIapResolveDependencyVersion('androidx.test.espresso:espresso-core', 'openIapEspressoCoreVersion') + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -23,17 +34,13 @@ if (flutterVersionName == null) { } android { - namespace 'dev.hyo.martie' - compileSdkVersion 34 - ndkVersion "27.0.12077973" + namespace = 'dev.hyo.martie' + compileSdk = openIapCompileSdkVersion + ndkVersion = "27.0.12077973" compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } lintOptions { @@ -42,12 +49,12 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "dev.hyo.martie" - minSdkVersion flutter.minSdkVersion - targetSdkVersion 34 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + applicationId = "dev.hyo.martie" + minSdkVersion = openIapMinSdkVersion + targetSdkVersion = openIapTargetSdkVersion + versionCode = flutterVersionCode.toInteger() + versionName = flutterVersionName + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" // Read horizonEnabled flag from gradle.properties (default: false) def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false @@ -68,17 +75,23 @@ android { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + signingConfig = signingConfigs.debug } } } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + flutter { - source '../..' + source = '../..' } dependencies { - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test:runner:1.5.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + testImplementation "junit:junit:$openIapJunitVersion" + androidTestImplementation "androidx.test:runner:$openIapAndroidTestRunnerVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$openIapEspressoCoreVersion" } diff --git a/libraries/flutter_inapp_purchase/example/android/app/src/main/AndroidManifest.xml b/libraries/flutter_inapp_purchase/example/android/app/src/main/AndroidManifest.xml index 14615d64..b1f423cd 100644 --- a/libraries/flutter_inapp_purchase/example/android/app/src/main/AndroidManifest.xml +++ b/libraries/flutter_inapp_purchase/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + + + 8.3.0 + 2.10.1 + 2.2.10 + 1.9.0.3 + 3.0.0 + 3.1.8 + 3.1.8 + 18.5.0 + 18.9.0 + 19.0.0 + 18.2.0 + + diff --git a/libraries/maui-iap/src/OpenIap.Maui.Bindings.Android/OpenIap.Maui.Bindings.Android.csproj b/libraries/maui-iap/src/OpenIap.Maui.Bindings.Android/OpenIap.Maui.Bindings.Android.csproj index 855c229e..2cb2faab 100644 --- a/libraries/maui-iap/src/OpenIap.Maui.Bindings.Android/OpenIap.Maui.Bindings.Android.csproj +++ b/libraries/maui-iap/src/OpenIap.Maui.Bindings.Android/OpenIap.Maui.Bindings.Android.csproj @@ -39,7 +39,7 @@ - - - + + - - - - - - - - + + + + + + + + - - - - + + + + - - - - + + + + diff --git a/libraries/maui-iap/src/OpenIap.Maui/Platforms/iOS/NSObjectJsonBridge.cs b/libraries/maui-iap/src/OpenIap.Maui/Platforms/iOS/NSObjectJsonBridge.cs index ef61b2ef..3cc91cc7 100644 --- a/libraries/maui-iap/src/OpenIap.Maui/Platforms/iOS/NSObjectJsonBridge.cs +++ b/libraries/maui-iap/src/OpenIap.Maui/Platforms/iOS/NSObjectJsonBridge.cs @@ -5,6 +5,7 @@ #nullable enable using System; +using System.Collections.Generic; using System.Text.Json.Nodes; using Foundation; @@ -50,6 +51,20 @@ internal static class NSObjectJsonBridge return json; } + public static NSDictionary JsonObjectToDictionary(JsonObject json) + { + var keys = new List(); + var values = new List(); + + foreach (var (key, node) in json) + { + keys.Add(new NSString(key)); + values.Add(JsonToNSObject(node) ?? NSNull.Null); + } + + return NSDictionary.FromObjectsAndKeys(values.ToArray(), keys.ToArray()); + } + public static JsonArray? ArrayToArray(NSArray? array) { if (array is null) return null; @@ -66,6 +81,38 @@ internal static class NSObjectJsonBridge private static JsonNode? ArrayToNode(NSArray array) => ArrayToArray(array); + private static NSObject? JsonToNSObject(JsonNode? node) + { + return node switch + { + null => null, + JsonObject obj => JsonObjectToDictionary(obj), + JsonArray array => JsonArrayToNSArray(array), + JsonValue value => JsonValueToNSObject(value), + _ => null, + }; + } + + private static NSArray JsonArrayToNSArray(JsonArray array) + { + var values = new List(); + foreach (var item in array) + { + values.Add(JsonToNSObject(item) ?? NSNull.Null); + } + return NSArray.FromNSObjects(values.ToArray()); + } + + private static NSObject JsonValueToNSObject(JsonValue value) + { + if (value.TryGetValue(out var boolValue)) return NSNumber.FromBoolean(boolValue); + if (value.TryGetValue(out var intValue)) return NSNumber.FromInt32(intValue); + if (value.TryGetValue(out var longValue)) return NSNumber.FromInt64(longValue); + if (value.TryGetValue(out var doubleValue)) return NSNumber.FromDouble(doubleValue); + if (value.TryGetValue(out var stringValue)) return new NSString(stringValue); + return new NSString(value.ToString() ?? string.Empty); + } + private static JsonNode? NumberToNode(NSNumber n) { // ObjCType encodes the underlying primitive: 'c' = char/BOOL, 'i' = int, 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 d0dba461..37613607 100644 --- a/libraries/maui-iap/src/OpenIap.Maui/Platforms/iOS/OpenIapIOS.cs +++ b/libraries/maui-iap/src/OpenIap.Maui/Platforms/iOS/OpenIapIOS.cs @@ -274,104 +274,19 @@ public Task EndConnectionAsync() { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - // Purchase request → fetch sku/quantity from the iOS sub-prop. - if (@params.RequestPurchase is { } purchaseEnv) + try { - var iosProps = purchaseEnv.Apple ?? purchaseEnv.IOS; - if (iosProps is null) - { - tcs.TrySetException(OpenIapErrorMapper.Wrap(ErrorCode.DeveloperError, "iOS purchase request requires `apple` props")); - return tcs.Task; - } - _module.RequestPurchase( - iosProps.Sku, - iosProps.Quantity ?? 1, - ProductTypeWireString(@params.Type), - (result, err) => - { - try - { - if (err is not null) { tcs.TrySetException(MapNSError(err)); return; } - var node = result is NSDictionary d ? NSObjectJsonBridge.DictToObject(d) : null; - if (node is null) { tcs.TrySetResult(null); return; } - var purchase = node.Deserialize(JsonOptions.Default); - tcs.TrySetResult(purchase is null ? null : new RequestPurchaseResultPurchase(purchase)); - } - catch (Exception ex) { tcs.TrySetException(ex); } - }); - return tcs.Task; + ValidateIosPurchaseRequest(@params); + var payload = RequestPurchasePayload(@params); + _module.RequestPurchaseWithPayload( + payload, + (result, err) => CompleteRequestPurchaseResult(tcs, result, err)); } - - if (@params.RequestSubscription is { } subEnv) + catch (Exception ex) { - var iosProps = subEnv.Apple ?? subEnv.IOS; - if (iosProps is null) - { - tcs.TrySetException(OpenIapErrorMapper.Wrap(ErrorCode.DeveloperError, "iOS subscription request requires `apple` props")); - return tcs.Task; - } - - NSDictionary? legacyOffer = iosProps.WithOffer is null - ? null - : NSDictionary.FromObjectsAndKeys( - new NSObject[] - { - new NSString(iosProps.WithOffer.Identifier), - new NSString(iosProps.WithOffer.KeyIdentifier), - new NSString(iosProps.WithOffer.Nonce), - new NSString(iosProps.WithOffer.Signature), - NSNumber.FromDouble(iosProps.WithOffer.Timestamp), - }, - new NSObject[] - { - new NSString("identifier"), - new NSString("keyIdentifier"), - new NSString("nonce"), - new NSString("signature"), - new NSString("timestamp"), - }); - - NSDictionary? jws = iosProps.PromotionalOfferJws is null - ? null - : NSDictionary.FromObjectsAndKeys( - new NSObject[] - { - new NSString(iosProps.PromotionalOfferJws.OfferId), - new NSString(iosProps.PromotionalOfferJws.Jws), - }, - new NSObject[] - { - new NSString("offerId"), - new NSString("jws"), - }); - - NSNumber? introEligibility = iosProps.IntroductoryOfferEligibility is { } b - ? NSNumber.FromBoolean(b) - : null; - string? winBackId = iosProps.WinBackOffer?.OfferId; - - _module.RequestSubscriptionExtended( - iosProps.Sku, - legacyOffer!, - introEligibility!, - jws!, - winBackId!, - (result, err) => - { - try - { - if (err is not null) { tcs.TrySetException(MapNSError(err)); return; } - var node = result is NSDictionary d ? NSObjectJsonBridge.DictToObject(d) : null; - if (node is null) { tcs.TrySetResult(null); return; } - var purchase = node.Deserialize(JsonOptions.Default); - tcs.TrySetResult(purchase is null ? null : new RequestPurchaseResultPurchase(purchase)); - } - catch (Exception ex) { tcs.TrySetException(ex); } - }); - return tcs.Task; + tcs.TrySetException(ex); } - tcs.TrySetException(OpenIapErrorMapper.Wrap(ErrorCode.DeveloperError, "RequestPurchaseProps must set requestPurchase or requestSubscription")); return tcs.Task; } @@ -587,12 +502,19 @@ public async Task> GetActiveSubscriptionsAsync return result.Where(a => filter.Contains(a.ProductId)).ToList(); } - public Task HasActiveSubscriptionsAsync(IReadOnlyList? subscriptionIds = null) - => InvokeBool(cb => _module.HasActiveSubscriptions(cb)); + public async Task HasActiveSubscriptionsAsync(IReadOnlyList? subscriptionIds = null) + { + if (subscriptionIds is { Count: > 0 }) + { + return (await GetActiveSubscriptionsAsync(subscriptionIds)).Count > 0; + } + + return await InvokeBool(cb => _module.HasActiveSubscriptions(cb)); + } public async Task GetStorefrontAsync() { - var storefront = await InvokeNullableString(cb => _module.GetStorefrontIOS(cb)); + var storefront = await InvokeNullableString(cb => _module.GetStorefront(cb)); return storefront ?? string.Empty; } @@ -660,6 +582,57 @@ private static void Complete(TaskCompletionSource tcs, bool ok, NSError? e catch (Exception ex) { tcs.TrySetException(ex); } } + private static void ValidateIosPurchaseRequest(RequestPurchaseProps @params) + { + if (@params.RequestPurchase is { } purchaseEnv) + { + var iosProps = purchaseEnv.Apple ?? purchaseEnv.IOS; + if (iosProps is null) + throw OpenIapErrorMapper.Wrap(ErrorCode.DeveloperError, "iOS purchase request requires `apple` props"); + if (string.IsNullOrWhiteSpace(iosProps.Sku)) + throw OpenIapErrorMapper.Wrap(ErrorCode.DeveloperError, "iOS purchase request requires a SKU"); + return; + } + + if (@params.RequestSubscription is { } subEnv) + { + var iosProps = subEnv.Apple ?? subEnv.IOS; + if (iosProps is null) + throw OpenIapErrorMapper.Wrap(ErrorCode.DeveloperError, "iOS subscription request requires `apple` props"); + if (string.IsNullOrWhiteSpace(iosProps.Sku)) + throw OpenIapErrorMapper.Wrap(ErrorCode.DeveloperError, "iOS subscription request requires a SKU"); + return; + } + + throw OpenIapErrorMapper.Wrap(ErrorCode.DeveloperError, "RequestPurchaseProps must set requestPurchase or requestSubscription"); + } + + private static NSDictionary RequestPurchasePayload(RequestPurchaseProps @params) + { + var node = JsonSerializer.SerializeToNode(@params, JsonOptions.Default) as JsonObject + ?? throw OpenIapErrorMapper.Wrap(ErrorCode.DeveloperError, "Unable to serialize RequestPurchaseProps"); + return NSObjectJsonBridge.JsonObjectToDictionary(node); + } + + private static void CompleteRequestPurchaseResult( + TaskCompletionSource tcs, + NSObject? result, + NSError? err) + { + try + { + if (err is not null) { tcs.TrySetException(MapNSError(err)); return; } + var node = result is NSDictionary d ? NSObjectJsonBridge.DictToObject(d) : null; + if (node is null) { tcs.TrySetResult(null); return; } + var purchase = node.Deserialize(JsonOptions.Default); + tcs.TrySetResult(purchase is null ? null : new RequestPurchaseResultPurchase(purchase)); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + } + private static OpenIapException MapNSError(NSError err) { var message = GetNSErrorString(err, "message") @@ -684,9 +657,6 @@ private static OpenIapException MapNSError(NSError err) return err.UserInfo?.ObjectForKey(nsKey)?.ToString(); } - private static string? ProductTypeWireString(ProductQueryType type) - => type == ProductQueryType.Subs ? "subs" : type == ProductQueryType.InApp ? "in-app" : null; - private static NSDictionary ToPurchaseOptionsDictionary(PurchaseOptions? options) { var alsoPublish = options?.AlsoPublishToEventListenerIOS ?? false; diff --git a/libraries/maui-iap/src/OpenIap.Maui/Types.cs b/libraries/maui-iap/src/OpenIap.Maui/Types.cs index e89111dd..6c5ea212 100644 --- a/libraries/maui-iap/src/OpenIap.Maui/Types.cs +++ b/libraries/maui-iap/src/OpenIap.Maui/Types.cs @@ -4100,26 +4100,26 @@ public sealed record WinBackOfferInputIOS public interface MutationResolver { /// Acknowledge a non-consumable purchase. Required within 3 days or Google auto-refunds. - /// See: https://www.openiap.dev/docs/apis/android/acknowledge-purchase-android + /// See: https://openiap.dev/docs/apis/android/acknowledge-purchase-android Task AcknowledgePurchaseAndroidAsync(string purchaseToken); /// Present the refund request sheet (iOS 15+). See also Features → Refund. - /// See: https://www.openiap.dev/docs/apis/ios/begin-refund-request-ios + /// See: https://openiap.dev/docs/apis/ios/begin-refund-request-ios Task BeginRefundRequestIOSAsync(string sku); /// Check whether alternative billing is available for the user. Step 1 of the alternative billing flow. /// /// Returns true if available, false otherwise. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/check-alternative-billing-availability-android + /// See: https://openiap.dev/docs/apis/android/check-alternative-billing-availability-android Task CheckAlternativeBillingAvailabilityAndroidAsync(); /// Clear pending transactions in the queue (sandbox helper). - /// See: https://www.openiap.dev/docs/apis/ios/clear-transaction-ios + /// See: https://openiap.dev/docs/apis/ios/clear-transaction-ios Task ClearTransactionIOSAsync(); /// Consume a consumable purchase so it can be re-bought. - /// See: https://www.openiap.dev/docs/apis/android/consume-purchase-android + /// See: https://openiap.dev/docs/apis/android/consume-purchase-android Task ConsumePurchaseAndroidAsync(string purchaseToken); /// Create a reporting token for an alternative billing flow. Step 3 of the alternative billing flow. @@ -4128,7 +4128,7 @@ public interface MutationResolver /// /// Returns token string, or null if creation failed. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/create-alternative-billing-token-android + /// See: https://openiap.dev/docs/apis/android/create-alternative-billing-token-android Task CreateAlternativeBillingTokenAndroidAsync(); /// Create the reporting payload Google requires after a Developer-Provided Billing transaction (Play Billing 8.3.0+). @@ -4136,23 +4136,23 @@ public interface MutationResolver /// /// Returns external transaction token needed for reporting external transactions. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/create-billing-program-reporting-details-android + /// See: https://openiap.dev/docs/apis/android/create-billing-program-reporting-details-android Task CreateBillingProgramReportingDetailsAndroidAsync(BillingProgramAndroid program); /// Open the platform's subscription management UI. - /// See: https://www.openiap.dev/docs/apis/deep-link-to-subscriptions + /// See: https://openiap.dev/docs/apis/deep-link-to-subscriptions Task DeepLinkToSubscriptionsAsync(DeepLinkOptions? options = null); /// Close the store connection and release resources. - /// See: https://www.openiap.dev/docs/apis/end-connection + /// See: https://openiap.dev/docs/apis/end-connection Task EndConnectionAsync(); /// Complete a transaction after server-side verification. Required on Android within 3 days. - /// See: https://www.openiap.dev/docs/apis/finish-transaction + /// See: https://openiap.dev/docs/apis/finish-transaction Task FinishTransactionAsync(PurchaseInput purchase, bool? isConsumable = null); /// Initialize the store connection. Call before any IAP API. - /// See: https://www.openiap.dev/docs/apis/init-connection + /// See: https://openiap.dev/docs/apis/init-connection Task InitConnectionAsync(InitConnectionConfig? config = null); /// Check whether a billing program (e.g., External Payments) is available for the current user. @@ -4161,7 +4161,7 @@ public interface MutationResolver /// Available in Google Play Billing Library 8.2.0+. /// Returns availability result with isAvailable flag. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/is-billing-program-available-android + /// See: https://openiap.dev/docs/apis/android/is-billing-program-available-android Task IsBillingProgramAvailableAndroidAsync(BillingProgramAndroid program); /// Launch an external content/offer link from inside the Billing Programs flow (Play Billing 8.2.0+). @@ -4169,25 +4169,25 @@ public interface MutationResolver /// /// Shows Play Store dialog and optionally launches external URL. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/launch-external-link-android + /// See: https://openiap.dev/docs/apis/android/launch-external-link-android Task LaunchExternalLinkAndroidAsync(LaunchExternalLinkParamsAndroid @params); /// Show the App Store offer code redemption sheet. - /// See: https://www.openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios Task PresentCodeRedemptionSheetIOSAsync(); /// Present an external purchase link, StoreKit External (iOS 16+). - /// See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-link-ios + /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-link-ios Task PresentExternalPurchaseLinkIOSAsync(string url); /// Present the external purchase notice sheet (iOS 17.4+). /// Uses ExternalPurchase.presentNoticeSheet() which returns a token when the user continues. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchase/presentnoticesheet() - /// See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios + /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios Task PresentExternalPurchaseNoticeSheetIOSAsync(); /// Initiate a purchase or subscription flow; rely on events for final state. - /// See: https://www.openiap.dev/docs/apis/request-purchase + /// See: https://openiap.dev/docs/apis/request-purchase Task RequestPurchaseAsync(RequestPurchaseProps @params); /// Buy the currently promoted product. @@ -4195,11 +4195,11 @@ public interface MutationResolver /// @deprecated Use promotedProductListenerIOS to receive the productId, /// then call requestPurchase with that SKU instead. In StoreKit 2, /// promoted products can be purchased directly via the standard purchase flow. - /// See: https://www.openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios + /// See: https://openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios Task RequestPurchaseOnPromotedProductIOSAsync(); /// Restore non-consumable and active subscription purchases. - /// See: https://www.openiap.dev/docs/apis/restore-purchases + /// See: https://openiap.dev/docs/apis/restore-purchases Task RestorePurchasesAsync(); /// Display Google's alternative billing information dialog. Step 2 of the alternative billing flow. @@ -4207,25 +4207,25 @@ public interface MutationResolver /// /// Returns true if user accepted, false if user canceled. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/show-alternative-billing-dialog-android + /// See: https://openiap.dev/docs/apis/android/show-alternative-billing-dialog-android Task ShowAlternativeBillingDialogAndroidAsync(); /// Present the disclosure sheet required before linking out via ExternalPurchaseCustomLink (iOS 18.1+). /// Call this after a deliberate customer interaction before linking out to external purchases. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/shownotice(type:) - /// See: https://www.openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios + /// See: https://openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios Task ShowExternalPurchaseCustomLinkNoticeIOSAsync(ExternalPurchaseCustomLinkNoticeTypeIOS noticeType); /// Present the manage-subscriptions sheet and return changed purchases (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/show-manage-subscriptions-ios + /// See: https://openiap.dev/docs/apis/ios/show-manage-subscriptions-ios Task> ShowManageSubscriptionsIOSAsync(); /// Force sync transactions with the App Store (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/sync-ios + /// See: https://openiap.dev/docs/apis/ios/sync-ios Task SyncIOSAsync(); /// Deprecated. Validate purchase receipts with the configured providers — use verifyPurchase instead. - /// See: https://www.openiap.dev/docs/features/validation#verify-purchase + /// See: https://openiap.dev/docs/features/validation#verify-purchase Task ValidateReceiptAsync(VerifyPurchaseProps options); /// Verify a purchase against your own backend. Returns a platform-specific @@ -4233,13 +4233,13 @@ public interface MutationResolver /// + receipt/JWS metadata, VerifyPurchaseResultAndroid carries Play Store /// receipt fields (no isValid), and VerifyPurchaseResultHorizon uses success. /// Inspect the concrete variant before reading fields. - /// See: https://www.openiap.dev/docs/features/validation#verify-purchase + /// See: https://openiap.dev/docs/features/validation#verify-purchase Task VerifyPurchaseAsync(VerifyPurchaseProps options); /// Verify via a managed provider without standing up your own server. The /// PurchaseVerificationProvider enum currently exposes only IAPKit; platform /// availability may differ by implementation. - /// See: https://www.openiap.dev/docs/features/validation#verify-purchase-with-provider + /// See: https://openiap.dev/docs/features/validation#verify-purchase-with-provider Task VerifyPurchaseWithProviderAsync(VerifyPurchaseWithProviderProps options); } @@ -4248,94 +4248,94 @@ public interface QueryResolver { /// Check eligibility for the external purchase notice sheet (iOS 17.4+). /// Uses ExternalPurchase.canPresent. - /// See: https://www.openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios + /// See: https://openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios Task CanPresentExternalPurchaseNoticeIOSAsync(); /// Get the user's current entitlement for a product, using StoreKit 2 (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/current-entitlement-ios + /// See: https://openiap.dev/docs/apis/ios/current-entitlement-ios Task CurrentEntitlementIOSAsync(string sku); /// Fetch products or subscriptions from the store. - /// See: https://www.openiap.dev/docs/apis/fetch-products + /// See: https://openiap.dev/docs/apis/fetch-products Task FetchProductsAsync(ProductRequest @params); /// Get details of all currently active subscriptions (filters by subscriptionIds when provided). - /// See: https://www.openiap.dev/docs/apis/get-active-subscriptions + /// See: https://openiap.dev/docs/apis/get-active-subscriptions Task> GetActiveSubscriptionsAsync(IReadOnlyList? subscriptionIds = null); /// List every StoreKit transaction (finished + unfinished) for the current user. /// Requires the SK2ConsumableTransactionHistory Info.plist key in the host app /// for finished consumables to be included (iOS 18+). /// Unlike getAvailablePurchases, always returns the iOS-specific PurchaseIOS shape. - /// See: https://www.openiap.dev/docs/apis/ios/get-all-transactions-ios + /// See: https://openiap.dev/docs/apis/ios/get-all-transactions-ios Task> GetAllTransactionsIOSAsync(); /// Fetch the app transaction (iOS 16+). - /// See: https://www.openiap.dev/docs/apis/ios/get-app-transaction-ios + /// See: https://openiap.dev/docs/apis/ios/get-app-transaction-ios Task GetAppTransactionIOSAsync(); /// List active purchases for the current user. - /// See: https://www.openiap.dev/docs/apis/get-available-purchases + /// See: https://openiap.dev/docs/apis/get-available-purchases Task> GetAvailablePurchasesAsync(PurchaseOptions? options = null); /// Fetch a token for Apple's External Purchase Server reporting API (iOS 18.1+). /// Use this token to report transactions made through ExternalPurchaseCustomLink. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) - /// See: https://www.openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios + /// See: https://openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios Task GetExternalPurchaseCustomLinkTokenIOSAsync(ExternalPurchaseCustomLinkTokenTypeIOS tokenType); /// List unfinished StoreKit transactions in the queue. - /// See: https://www.openiap.dev/docs/apis/ios/get-pending-transactions-ios + /// See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios Task> GetPendingTransactionsIOSAsync(); /// Read the App Store-promoted product, if any (iOS 11+). - /// See: https://www.openiap.dev/docs/apis/ios/get-promoted-product-ios + /// See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios Task GetPromotedProductIOSAsync(); /// Get base64-encoded receipt data (legacy validation). - /// See: https://www.openiap.dev/docs/apis/ios/get-receipt-data-ios + /// See: https://openiap.dev/docs/apis/ios/get-receipt-data-ios Task GetReceiptDataIOSAsync(); /// Return the user's storefront country code. - /// See: https://www.openiap.dev/docs/apis/get-storefront + /// See: https://openiap.dev/docs/apis/get-storefront Task GetStorefrontAsync(); /// Deprecated. Get the current App Store storefront country code — use cross-platform getStorefront instead. - /// See: https://www.openiap.dev/docs/apis/ios/get-storefront-ios + /// See: https://openiap.dev/docs/apis/ios/get-storefront-ios Task GetStorefrontIOSAsync(); /// Return the JWS string for a transaction (StoreKit 2). - /// See: https://www.openiap.dev/docs/apis/ios/get-transaction-jws-ios + /// See: https://openiap.dev/docs/apis/ios/get-transaction-jws-ios Task GetTransactionJwsIOSAsync(string sku); /// Check whether the user has any active subscription. - /// See: https://www.openiap.dev/docs/apis/has-active-subscriptions + /// See: https://openiap.dev/docs/apis/has-active-subscriptions Task HasActiveSubscriptionsAsync(IReadOnlyList? subscriptionIds = null); /// Check eligibility for the custom-link variant of external purchase (iOS 18.1+). /// Returns true if the app can use custom external purchase links. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/iseligible - /// See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios + /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios Task IsEligibleForExternalPurchaseCustomLinkIOSAsync(); /// Check intro-offer eligibility for a subscription group. - /// See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios + /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios Task IsEligibleForIntroOfferIOSAsync(string groupId); /// Check whether a transaction's JWS verification passed (StoreKit 2). - /// See: https://www.openiap.dev/docs/apis/ios/is-transaction-verified-ios + /// See: https://openiap.dev/docs/apis/ios/is-transaction-verified-ios Task IsTransactionVerifiedIOSAsync(string sku); /// Get the latest verified transaction for a product, using StoreKit 2. - /// See: https://www.openiap.dev/docs/apis/ios/latest-transaction-ios + /// See: https://openiap.dev/docs/apis/ios/latest-transaction-ios Task LatestTransactionIOSAsync(string sku); /// Get subscription status objects from StoreKit 2 (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/subscription-status-ios + /// See: https://openiap.dev/docs/apis/ios/subscription-status-ios Task> SubscriptionStatusIOSAsync(string sku); /// Deprecated. Legacy App Store receipt validation — use verifyPurchase instead. - /// See: https://www.openiap.dev/docs/apis/ios/validate-receipt-ios + /// See: https://openiap.dev/docs/apis/ios/validate-receipt-ios Task ValidateReceiptIOSAsync(VerifyPurchaseProps options); } diff --git a/libraries/react-native-iap/CLAUDE.md b/libraries/react-native-iap/CLAUDE.md index cebfea16..2e70b2f5 100644 --- a/libraries/react-native-iap/CLAUDE.md +++ b/libraries/react-native-iap/CLAUDE.md @@ -17,11 +17,10 @@ React Native IAP - A high-performance in-app purchase library using Nitro Module ⚠️ **IMPORTANT: This project uses Yarn 3 with workspaces** -- **Workspace Structure**: Only `example` is in the yarn workspace. `example-expo` is an independent project +- **Workspace Structure**: Only `example` is in the yarn workspace - Install dependencies: `yarn install` (installs for library and example only) - Add packages to library: `yarn add [package]` - Add packages to example: `yarn workspace rn-iap-example add [package]` -- Add packages to example-expo: `cd example-expo && bun add [package]` (independent) - Run scripts: `yarn [script]` - Execute packages: `yarn dlx [package]` or `npx [package]` @@ -51,10 +50,6 @@ example/ # React Native example app (workspace) ├── android/ └── package.json -example-expo/ # Independent Expo example app (NOT in workspace) -├── app/ -├── scripts/ -└── package.json # Uses bun, independent from yarn workspace ``` ### Auto-generated Files @@ -103,26 +98,6 @@ yarn workspace rn-iap-example android yarn workspace rn-iap-example start ``` -### Example-Expo (Independent Project) - -```bash -# Independent project - requires separate setup -# Uses bun and expo setup script - -# Initial setup (copies lib files and builds) -cd example-expo && bun setup - -# iOS -cd example-expo && bun ios -cd example-expo && bun ios --device # For physical device - -# Android -cd example-expo && bun android - -# Start Metro bundler -cd example-expo && bun start -``` - ### iOS Setup ```bash @@ -130,10 +105,6 @@ cd example-expo && bun start cd example/ios bundle install # Install Ruby dependencies bundle exec pod install # Install iOS dependencies - -# For example-expo (independent) -cd example-expo/ios -pod install # iOS dependencies ``` ## VSCode Integration @@ -315,7 +286,7 @@ Both platforms use the OpenIAP library's error handling: Both Android and iOS now use **OpenIAP's unified error codes** (kebab-case format): - Examples: `user-cancelled`, `item-unavailable`, `network-error`, `developer-error` -- See [OpenIAP Error Handling](https://www.openiap.dev/api/error-handling) for complete list +- See [OpenIAP Error Handling](https://openiap.dev/api/error-handling) for complete list **TypeScript Error Format:** @@ -460,7 +431,6 @@ All error codes are defined in the `ErrorCode` enum: - **Additional Notes**: - Resolutions are configured in root `package.json` (workspace level) - - `example-expo` is **NOT** in yarn workspace (independent project) - Only `example` is included in workspace structure 3. **iOS build errors** diff --git a/libraries/react-native-iap/CONTRIBUTING.md b/libraries/react-native-iap/CONTRIBUTING.md index 9447b77f..3af5b58c 100644 --- a/libraries/react-native-iap/CONTRIBUTING.md +++ b/libraries/react-native-iap/CONTRIBUTING.md @@ -21,19 +21,13 @@ We welcome contributions! This guide will help you get started with development 3. **Running the Example App** ```bash - # React Native Example (workspace) cd example && yarn ios cd example && yarn android - - # Expo Example (independent) - cd example-expo && bun setup - cd example-expo && bun ios - cd example-expo && bun android ``` ## Example Apps Architecture -This project includes two example applications: +This project includes one React Native CLI example application: ### `example/` - React Native CLI Example @@ -42,45 +36,6 @@ This project includes two example applications: - **Package Manager**: Yarn (workspace) - **Purpose**: Main development and testing environment -### `example-expo/` - Expo Router Example - -- **Location**: `example-expo/app/` -- **Router**: Expo Router -- **Package Manager**: Bun (independent) -- **Purpose**: Expo-specific testing and demonstration - -### Screen File Synchronization - -The `example-expo` app automatically syncs screen files from `example/screens/` to maintain consistency: - -- **Source**: `example/screens/*.tsx` (PascalCase) -- **Target**: `example-expo/app/*.tsx` (kebab-case) - -**File Mapping:** - -- `AvailablePurchases.tsx` → `available-purchases.tsx` -- `OfferCode.tsx` → `offer-code.tsx` -- `PurchaseFlow.tsx` → `purchase-flow.tsx` -- `SubscriptionFlow.tsx` → `subscription-flow.tsx` - -**Automatic Synchronization:** - -```bash -# Happens automatically during: -cd example-expo && bun install # (postinstall script) -cd example-expo && bun setup # (setup script) - -# Manual synchronization: -cd example-expo && ./scripts/copy-screens.sh -``` - -**Important Notes:** - -- 🚨 **Do NOT edit files in `example-expo/app/` directly** -- ✅ **Always modify the source files in `example/screens/`** -- 🔄 **Files are automatically copied with generation comments** -- 📝 **Generated files include header comments indicating their source** - ## Code Guidelines - Run `yarn typecheck` and `yarn lint --fix` before committing @@ -94,9 +49,7 @@ When working with example screens: 1. **Modify Source Files**: Make changes in `example/screens/*.tsx` 2. **Test React Native**: Run `cd example && yarn ios/android` to test changes -3. **Sync Expo Files**: Changes automatically sync to `example-expo/app/` during `bun install` -4. **Test Expo**: Run `cd example-expo && bun ios/android` to verify expo compatibility -5. **Commit**: Only commit source files in `example/screens/`, not generated files in `example-expo/app/` +3. **Commit**: Commit the source files in `example/screens/` ### Testing @@ -110,8 +63,7 @@ yarn typecheck && yarn lint --fix yarn test:ci # Test example apps -cd example && yarn test # React Native example tests -cd example-expo && bun test # Expo example tests (if available) +cd example && yarn test ``` ## Release Process (Maintainers) @@ -144,14 +96,6 @@ Follow these steps when preparing a new release (e.g., 14.2.0): - Create a GitHub Release - Publish to npm via the existing workflows -Recent highlights (14.2.0) - -- iOS: idempotent, non-blocking init; `initConnection()` now propagates failures. -- iOS: bump OpenIAP to `~> 1.1.8`. -- Android: add consumer R8 keep rules to protect Nitro HybridObjects. -- CI: use vendored Yarn to avoid Corepack 503. -- Example: stabilized Subscription/Purchase flows; tests improved. - ## Project Structure - [`android/`](android): All your `android`-specific implementations. @@ -187,4 +131,4 @@ Recent highlights (14.2.0) 4. Run tests and linting: `yarn typecheck && yarn lint --fix` 5. Submit a pull request with a clear description -For detailed usage examples and error handling, see the [documentation](https://hyochan.github.io/react-native-iap). +For detailed usage examples and error handling, see the [documentation](https://openiap.dev/docs/setup/react-native). diff --git a/libraries/react-native-iap/README.md b/libraries/react-native-iap/README.md index f1f110b1..9daa6d56 100644 --- a/libraries/react-native-iap/README.md +++ b/libraries/react-native-iap/README.md @@ -1,7 +1,7 @@ # React Native IAP
- React Native IAP Logo + React Native IAP Logo [![Version](http://img.shields.io/npm/v/react-native-iap.svg?style=flat-square)](https://npmjs.org/package/react-native-iap) [![Download](http://img.shields.io/npm/dm/react-native-iap.svg?style=flat-square)](https://npmjs.org/package/react-native-iap) @@ -33,7 +33,7 @@ ## 📚 Documentation -**[📖 Visit our comprehensive documentation site →](https://hyochan.github.io/react-native-iap)** +**[📖 Visit our comprehensive documentation site →](https://openiap.dev/docs/setup/react-native)** ## ⚠️ Notice @@ -46,7 +46,7 @@ - Seeing Swift 6 C++ interop errors in Nitro (e.g., `AnyMap.swift` with `cppPart.pointee.*`)? Temporarily pin Swift to **5.10** for the `NitroModules` pod (see Installation docs) or upgrade RN and Nitro deps. - Recommended: upgrade to RN 0.79+, update `react-native-nitro-modules`/`nitro-codegen`, then `pod install` and clean build. -More details and the Podfile snippet are in the docs: https://hyochan.github.io/react-native-iap/docs/installation#ios +More details and the Podfile snippet are in the docs: https://openiap.dev/docs/setup/react-native#ios ## ✨ Features @@ -55,7 +55,7 @@ More details and the Podfile snippet are in the docs: https://hyochan.github.io/ - 🎯 **TypeScript First**: Full TypeScript support with comprehensive type definitions - 🛡️ **Centralized Error Handling**: Unified error management with platform-specific error code mapping - 🎣 **React Hooks**: Modern React hooks API with `useIAP` -- 📱 **Expo Compatible**: Works with Expo development builds +- 📱 **React Native Focused**: Use `expo-iap` for Expo projects - 🔍 **Receipt Validation**: Built-in receipt validation for both platforms - 💎 **Products & Subscriptions**: Support for both one-time purchases and subscriptions - 🚀 **Performance Optimized**: Efficient caching and minimal re-renders @@ -68,7 +68,7 @@ npm install react-native-iap react-native-nitro-modules yarn add react-native-iap react-native-nitro-modules ``` -**[📖 See the complete installation guide and quick start tutorial →](https://hyochan.github.io/react-native-iap/docs/installation)** +**[📖 See the complete installation guide and quick start tutorial →](https://openiap.dev/docs/setup/react-native#installation)** ## 🏗️ Architecture @@ -82,13 +82,13 @@ React Native IAP is built with a modern architecture that emphasizes: ## 📱 Platform Support -| Platform | Support | Notes | -| ----------------- | ------- | --------------------------------------- | -| iOS | ✅ | StoreKit 2 (requires iOS 15+) | -| Android | ✅ | Google Play Billing v8.0.0+ | -| Expo Go | ❌ | Not supported (requires native modules) | -| Expo Dev Client | ✅ | Full support | -| Bare React Native | ✅ | Full support | +| Platform | Support | Notes | +| ----------------- | ------- | --------------------------------- | +| iOS | ✅ | StoreKit 2 (requires iOS 15+) | +| Android | ✅ | Google Play Billing v8.0.0+ | +| Expo Go | ❌ | Use `expo-iap` for Expo projects | +| Expo Dev Client | ❌ | Use `expo-iap` for Expo projects | +| Bare React Native | ✅ | Full support | ## 📦 Installation & Configuration @@ -96,7 +96,7 @@ React Native IAP is built with a modern architecture that emphasizes: Before installing React Native IAP, make sure you have: -- React Native 0.64 or later, or Expo SDK 45 or later +- React Native 0.64 or later - Node.js 16 or later - iOS 15+ for iOS apps (StoreKit 2 requirement) - Android API level 21+ for Android apps @@ -112,7 +112,7 @@ In your root `android/build.gradle`: ```gradle buildscript { ext { - kotlinVersion = "2.1.20" + kotlinVersion = "2.2.0" } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" @@ -134,59 +134,39 @@ buildscript { - Go to "Signing & Capabilities" - Click "+ Capability" and add "In-App Purchase" -#### Expo Configuration - -For Expo projects, add the plugin to your `app.json` or `expo.json`: - -```json -{ - "expo": { - "plugins": [ - "react-native-iap", - [ - "expo-build-properties", - { - "android": { - "kotlinVersion": "2.2.0" - } - } - ] - ] - } -} -``` +#### Expo Projects -**Note:** Expo projects require [development build (dev-client)](https://docs.expo.dev/develop/development-builds/introduction/) as this library contains native code. +Use [`expo-iap`](https://github.com/hyodotdev/openiap/tree/main/libraries/expo-iap) for Expo apps. This package targets bare React Native/Nitro projects. ### Store Configuration React Native IAP is **OpenIAP compliant**. For detailed store configuration: -- **[iOS Setup →](https://www.openiap.dev/docs/ios-setup)** - App Store Connect configuration -- **[Android Setup →](https://www.openiap.dev/docs/android-setup)** - Google Play Console configuration +- **[iOS Setup →](https://openiap.dev/docs/ios-setup)** - App Store Connect configuration +- **[Android Setup →](https://openiap.dev/docs/android-setup)** - Google Play Console configuration ## 🤖 Using with AI Assistants React Native IAP provides AI-friendly documentation for Cursor, GitHub Copilot, Claude, and ChatGPT. -**[📖 AI Assistants Guide →](https://hyochan.github.io/react-native-iap/docs/guides/ai-assistants)** +**[📖 AI Assistants Guide →](https://openiap.dev/docs/guides/ai-assistants)** Quick links: -- [llms.txt](https://hyochan.github.io/react-native-iap/llms.txt) - Quick reference -- [llms-full.txt](https://hyochan.github.io/react-native-iap/llms-full.txt) - Full API reference +- [llms.txt](https://openiap.dev/llms.txt) - Quick reference +- [llms-full.txt](https://openiap.dev/llms-full.txt) - Full API reference ## 🎯 What's Next? -**[📖 Visit our comprehensive documentation site →](https://hyochan.github.io/react-native-iap)** +**[📖 Visit our comprehensive documentation site →](https://openiap.dev/docs/setup/react-native)** ### Key Resources -- **[Installation & Quick Start](https://hyochan.github.io/react-native-iap/docs/installation)** - Get started in minutes -- **[API Reference](https://hyochan.github.io/react-native-iap/docs/api)** - Complete useIAP hook documentation -- **[Examples](https://hyochan.github.io/react-native-iap/docs/examples/basic-store)** - Production-ready implementations -- **[Error Handling](https://hyochan.github.io/react-native-iap/docs/api/error-codes)** - OpenIAP compliant error codes -- **[Troubleshooting](https://hyochan.github.io/react-native-iap/docs/guides/troubleshooting)** - Common issues and solutions +- **[Installation & Quick Start](https://openiap.dev/docs/setup/react-native#installation)** - Get started in minutes +- **[API Reference](https://openiap.dev/docs/apis)** - Complete useIAP hook documentation +- **[Examples](https://openiap.dev/docs/example)** - Production-ready implementations +- **[Error Handling](https://openiap.dev/docs/errors)** - OpenIAP compliant error codes +- **[Troubleshooting](https://openiap.dev/docs/features/debugging)** - Common issues and solutions ## Powered by OpenIAP @@ -217,10 +197,10 @@ Other libraries built on OpenIAP: [expo-iap](https://github.com/hyodotdev/openia diff --git a/libraries/react-native-iap/android/build.gradle b/libraries/react-native-iap/android/build.gradle index bdd2c731..28c4de04 100644 --- a/libraries/react-native-iap/android/build.gradle +++ b/libraries/react-native-iap/android/build.gradle @@ -1,14 +1,50 @@ import groovy.json.JsonSlurper +import org.jetbrains.kotlin.gradle.dsl.JvmTarget buildscript { + def googleRootBuildFile = [ + new File(projectDir, '../../../packages/google/build.gradle.kts'), + new File(rootDir, '../../../packages/google/build.gradle.kts'), + new File(rootProject.projectDir, '../../../packages/google/build.gradle.kts') + ].find { it.exists() } + + def googlePluginVersion = { pluginId -> + if (googleRootBuildFile == null) { + return null + } + def marker = "id(\"${pluginId}\") version \"" + def line = googleRootBuildFile.readLines().find { it.contains(marker) } + if (line == null) { + return null + } + def matcher = line =~ /version "([^"]+)"/ + return matcher.find() ? matcher.group(1) : null + } + + def configuredVersion = { extName, propertyName -> + if (rootProject.ext.has(extName)) { + return rootProject.ext.get(extName).toString() + } + def propertyValue = project.findProperty(propertyName) + if (propertyValue == null || propertyValue.toString().trim().isEmpty()) { + throw new GradleException("react-native-iap: missing ${propertyName} in android/gradle.properties") + } + return propertyValue.toString() + } + + def androidGradlePluginVersion = googlePluginVersion('com.android.library') + ?: configuredVersion('androidGradlePluginVersion', 'NitroIap_androidGradlePluginVersion') + def kotlinGradlePluginVersion = googlePluginVersion('org.jetbrains.kotlin.android') + ?: configuredVersion('kotlinVersion', 'NitroIap_kotlinVersion') + repositories { google() mavenCentral() } dependencies { - classpath "com.android.tools.build:gradle:8.12.1" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20" + classpath "com.android.tools.build:gradle:$androidGradlePluginVersion" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinGradlePluginVersion" } } @@ -45,9 +81,6 @@ def googleVersionString = googleVersion.trim() apply plugin: "com.android.library" apply plugin: 'org.jetbrains.kotlin.android' -// Get kotlinVersion from root project or use default -def kotlinVersion = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : '2.0.21' - // Only apply Nitro autolinking if the file exists def nitroAutolinkingFile = file('../nitrogen/generated/android/NitroIap+autolinking.gradle') if (nitroAutolinkingFile.exists()) { @@ -63,25 +96,61 @@ apply from: "./fix-prefab.gradle" // } def getExtOrDefault(name) { - return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["NitroIap_" + name] + def value = rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["NitroIap_" + name] + if (value == null || value.toString().trim().isEmpty()) { + throw new GradleException("react-native-iap: missing NitroIap_${name} in android/gradle.properties") + } + return value } def getExtOrIntegerDefault(name) { - return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["NitroIap_" + name]).toInteger() + return getExtOrDefault(name).toString().toInteger() } // Read horizonEnabled from gradle.properties, default to false (play) def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false +def resolveOpenIapGoogleBuildFile() { + def candidates = [ + new File(projectDir, '../../../packages/google/openiap/build.gradle.kts'), + new File(rootDir, '../../../packages/google/openiap/build.gradle.kts'), + new File(rootProject.projectDir, '../../../packages/google/openiap/build.gradle.kts') + ] + return candidates.find { it.exists() } +} + +def readOpenIapGoogleVariable(File buildFile, String variableName) { + if (buildFile == null) { + return null + } + def matcher = buildFile.text =~ /val\s+${variableName}\s*=\s*"([^"]+)"/ + return matcher.find() ? matcher.group(1) : null +} + +def readOpenIapGoogleDependencyVersion(File buildFile, String coordinate) { + if (buildFile == null) { + return null + } + def matcher = buildFile.text =~ /${java.util.regex.Pattern.quote(coordinate)}:([^"$)]+)/ + return matcher.find() ? matcher.group(1) : null +} + +def googleOpenIapBuildFile = resolveOpenIapGoogleBuildFile() +def coroutinesVersion = readOpenIapGoogleVariable(googleOpenIapBuildFile, 'coroutinesVersion') + ?: getExtOrDefault("coroutinesVersion") +def playServicesBaseVersion = getExtOrDefault("playServicesBaseVersion") +def junitVersion = readOpenIapGoogleDependencyVersion(googleOpenIapBuildFile, 'junit:junit') + ?: getExtOrDefault("junitVersion") + android { - namespace "com.margelo.nitro.iap" + namespace = "com.margelo.nitro.iap" - ndkVersion getExtOrDefault("ndkVersion") + ndkVersion = getExtOrDefault("ndkVersion") compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") defaultConfig { - minSdkVersion getExtOrIntegerDefault("minSdkVersion") - targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + minSdkVersion = getExtOrIntegerDefault("minSdkVersion") + targetSdkVersion = getExtOrIntegerDefault("targetSdkVersion") buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() // Ship consumer keep rules so Nitro HybridObjects aren't stripped in app release builds consumerProguardFiles 'consumer-rules.pro' @@ -136,13 +205,13 @@ android { } buildFeatures { - buildConfig true - prefab true + buildConfig = true + prefab = true } buildTypes { release { - minifyEnabled false + minifyEnabled = false } } @@ -151,11 +220,6 @@ android { targetCompatibility JavaVersion.VERSION_17 } - // Configure Kotlin compiler to match Java compatibility - kotlinOptions { - jvmTarget = "17" - } - lintOptions { disable "GradleCompatible" } @@ -163,6 +227,12 @@ android { // Removed sourceSets configuration for codegen as it's not needed for library modules } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + repositories { mavenCentral() google() @@ -181,10 +251,10 @@ dependencies { } // Google Play Services - implementation 'com.google.android.gms:play-services-base:18.5.0' + implementation "com.google.android.gms:play-services-base:$playServicesBaseVersion" // Kotlin coroutines - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" // Determine which OpenIAP dependency to use // In monorepo: use local packages/google source if available @@ -198,7 +268,7 @@ dependencies { } // Test dependencies - testImplementation 'junit:junit:4.13.2' + testImplementation "junit:junit:$junitVersion" testImplementation 'org.jetbrains.kotlin:kotlin-test' testImplementation 'org.jetbrains.kotlin:kotlin-test-junit' } diff --git a/libraries/react-native-iap/android/gradle.properties b/libraries/react-native-iap/android/gradle.properties index 6746dd37..04f6bfa2 100644 --- a/libraries/react-native-iap/android/gradle.properties +++ b/libraries/react-native-iap/android/gradle.properties @@ -1,4 +1,8 @@ -NitroIap_kotlinVersion=2.1.20 +NitroIap_kotlinVersion=2.2.0 +NitroIap_androidGradlePluginVersion=8.13.2 +NitroIap_coroutinesVersion=1.9.0 +NitroIap_playServicesBaseVersion=18.5.0 +NitroIap_junitVersion=4.13.2 NitroIap_minSdkVersion=23 NitroIap_targetSdkVersion=36 NitroIap_compileSdkVersion=36 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 f7576cf1..13169a54 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 @@ -793,7 +793,7 @@ class HybridRnIap : HybridRnIapSpec() { // Event listener methods override fun addPurchaseUpdatedListener( listener: (purchase: NitroPurchase) -> Unit, - options: NitroPurchaseUpdatedListenerOptions? + options: PurchaseUpdatedListenerOptions? ): Double { return synchronized(purchaseUpdatedListeners) { val token = nextPurchaseUpdatedListenerToken diff --git a/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/RnIapLog.kt b/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/RnIapLog.kt index 7b30c67b..e71e54ff 100644 --- a/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/RnIapLog.kt +++ b/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/RnIapLog.kt @@ -20,7 +20,9 @@ internal object RnIapLog { } fun debug(message: String) { - Log.d(TAG, message) + if (BuildConfig.DEBUG || Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, message) + } } fun warn(message: String) { diff --git a/libraries/react-native-iap/codecov.yml b/libraries/react-native-iap/codecov.yml index 7a2db0df..b978b874 100644 --- a/libraries/react-native-iap/codecov.yml +++ b/libraries/react-native-iap/codecov.yml @@ -57,7 +57,6 @@ ignore: - '**/__mocks__/**' - '**/nitrogen/**' - '**/node_modules/**' - - 'example-expo/**' - 'docs/**' - 'scripts/**' - 'lib/**' diff --git a/libraries/react-native-iap/example-expo/.env.example b/libraries/react-native-iap/example-expo/.env.example deleted file mode 100644 index 619c5fa5..00000000 --- a/libraries/react-native-iap/example-expo/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -# IAPKit Configuration -# Get your API key from https://kit.openiap.dev -EXPO_PUBLIC_IAPKIT_API_KEY=your_iapkit_api_key_here diff --git a/libraries/react-native-iap/example-expo/.gitignore b/libraries/react-native-iap/example-expo/.gitignore deleted file mode 100644 index adfbc8d8..00000000 --- a/libraries/react-native-iap/example-expo/.gitignore +++ /dev/null @@ -1,48 +0,0 @@ -# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files - -# dependencies -node_modules/ - -# Expo -.expo/ -dist/ -web-build/ -expo-env.d.ts - -# Native -*.orig.* -*.jks -*.p8 -*.p12 -*.key -*.mobileprovision -android/ -ios/ - -# Metro -.metro-health-check* - -# debug -npm-debug.* -yarn-debug.* -yarn-error.* - -# macOS -.DS_Store -*.pem - -# local env files -.env -.env*.local - -# typescript -*.tsbuildinfo - -app-example - -# Coverage reports -coverage/ -*.lcov - -# Test artifacts -__snapshots__/ diff --git a/libraries/react-native-iap/example-expo/.vscode/settings.json b/libraries/react-native-iap/example-expo/.vscode/settings.json deleted file mode 100644 index e2798e42..00000000 --- a/libraries/react-native-iap/example-expo/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "editor.codeActionsOnSave": { - "source.fixAll": "explicit", - "source.organizeImports": "explicit", - "source.sortMembers": "explicit" - } -} diff --git a/libraries/react-native-iap/example-expo/App.tsx b/libraries/react-native-iap/example-expo/App.tsx deleted file mode 100644 index 37f99756..00000000 --- a/libraries/react-native-iap/example-expo/App.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import {Slot} from 'expo-router'; - -export default function App() { - return ; -} diff --git a/libraries/react-native-iap/example-expo/README.md b/libraries/react-native-iap/example-expo/README.md deleted file mode 100644 index c15cb699..00000000 --- a/libraries/react-native-iap/example-expo/README.md +++ /dev/null @@ -1,232 +0,0 @@ -# React Native IAP - Expo Example - -This is an Expo example app for testing react-native-iap with the bundle identifier `dev.hyo.martie`. - -## ⚠️ Important: Independent Project Setup - -**This example app is NOT part of the yarn workspace and is managed independently.** - -### Why Independent Setup? - -After extensive testing, we discovered that Expo's autolinking system has fundamental conflicts with yarn workspaces when developing native modules: - -1. **Plugin Conflict**: Expo autolinking attempts to apply both `com.android.library` and `com.android.application` plugins to the same project, causing build failures -2. **Workspace Detection**: Expo autolinking incorrectly detects the parent library in a workspace structure and processes it as a nested module -3. **Persistent Error**: The error consistently occurs at line 333 of `expo-modules-autolinking/scripts/android/autolinking_implementation.gradle` - -These issues make it impossible to use Expo apps within yarn workspaces for native module development, hence this independent setup. - -## 🔧 How It Works - -### Package Management - -- This project uses **bun** for faster package management (independent from the root yarn workspace) -- The `react-native-iap` library is linked via a custom postinstall script - -### Postinstall Script - -The `postinstall` script (`scripts/postinstall.sh`) automatically sets up the development environment: - -1. **Creates Symlinks** for live development: - - `src/` → TypeScript source code (editable in root) - - `ios/` → iOS native code - - `android/` → Android native code - - `plugin/` → Expo plugin code - -2. **Copies Essential Files**: - - `package.json`, `README.md`, `tsconfig.json` - -3. **Builds the Library**: - - Runs `yarn prepare` in root directory - - Copies compiled files (`lib/`, `nitrogen/`) - -This allows you to: - -- ✅ Edit TypeScript files directly in `../src/` -- ✅ See changes immediately reflected in the example app -- ✅ Test native code modifications without republishing - -## Configuration - -- **Bundle Identifier (iOS)**: `dev.hyo.martie` -- **Package Name (Android)**: `dev.hyo.martie` -- **App Name**: Martie IAP Example - -## Prerequisites - -1. **iOS**: The app must be configured in App Store Connect with the bundle ID `dev.hyo.martie` -2. **Android**: The app must be configured in Google Play Console with the package name `dev.hyo.martie` -3. **Products**: In-app purchase products must be created with IDs matching those in the app: - - `dev.hyo.martie.1bulbs` (consumable product) - - `dev.hyo.martie.premium` (subscription) - -## 🚀 Getting Started - -### Quick Setup (Automated) - RECOMMENDED - -From the **root directory** of the main project: - -```bash -# Complete automated setup -yarn setup:expo - -# This single command will: -# ✅ Install all root dependencies -# ✅ Install example-expo dependencies -# ✅ Run postinstall script (linking, building, prebuild) -# ✅ Generate native iOS/Android code -# ✅ Everything ready to run! -``` - -After this, you can immediately run: - -```bash -cd example-expo -bun ios --device # or bun ios for simulator -bun android -``` - -### Manual Installation (if needed) - -```bash -# Install dependencies (this will run postinstall automatically) -bun install - -# If you need to manually run the setup: -./scripts/postinstall.sh -``` - -## Running the Example - -### Prebuild (Required First Time) - -```bash -# Generate native code -bunx expo prebuild -``` - -### iOS - -```bash -# Run on iOS simulator -bun run ios - -# Or for device -bun run ios --device -``` - -### Android - -```bash -# Run on Android emulator/device -bun run android -``` - -### Development Server Only - -```bash -# Start Metro bundler -bun start -``` - -## 🔄 Development Workflow - -### For TypeScript Changes - -1. **Edit TypeScript code** in the root `../src/` directory -2. **Rebuild the library**: - - ```bash - cd .. - yarn prepare - ``` - -3. **Restart Metro** (or it should hot-reload automatically) - -### For Native Code Changes - -1. **iOS**: After modifying iOS native code - - ```bash - cd ios - pod install - cd .. - bun run ios - ``` - -2. **Android**: After modifying Android native code - - ```bash - bun run android - ``` - -### Development Build (For Physical Testing) - -For testing in-app purchases on real devices: - -```bash -# iOS Device -npx eas build --profile preview --platform ios - -# Android Device -npx eas build --profile preview --platform android - -# Or build locally -npx eas build --profile development --platform ios --local -npx eas build --profile development --platform android --local -``` - -## Testing In-App Purchases - -### iOS Testing - -1. **Sandbox Testing**: - - Create sandbox tester accounts in App Store Connect - - Sign out of your personal Apple ID on the device - - Sign in with sandbox account when prompted during purchase - -2. **TestFlight**: - - Upload build to TestFlight for more realistic testing - - Real users can test without sandbox accounts - -### Android Testing - -1. **Testing Track**: - - Upload APK/AAB to internal/closed/open testing track - - Add test accounts in Google Play Console - -2. **License Testing**: - - Add test accounts in Play Console > Settings > License testing - - Test accounts can make purchases without being charged - -## Important Notes - -- The bundle identifier `dev.hyo.martie` must match exactly in: - - app.json configuration - - App Store Connect (iOS) - - Google Play Console (Android) - - Product IDs in the store consoles - -- Billing permissions are already configured in app.json: - - iOS: SKAdNetwork items for attribution - - Android: `com.android.vending.BILLING` permission - -## Troubleshooting - -### "Store not available" - -- Ensure you're testing on a real device (not simulator for purchases) -- Check that the bundle ID matches store configuration -- Verify products are approved and available in store console - -### "Product not found" - -- Product IDs must match exactly (case-sensitive) -- Products must be approved in store console -- Wait 24 hours after creating new products - -### Build Issues - -- Clear Metro cache: `yarn expo:start --clear` -- Clean build folders: `cd ios && rm -rf build && cd ..` -- Rebuild with `--clear-cache` flag diff --git a/libraries/react-native-iap/example-expo/app.config.ts b/libraries/react-native-iap/example-expo/app.config.ts deleted file mode 100644 index 1b97a093..00000000 --- a/libraries/react-native-iap/example-expo/app.config.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type {ConfigContext, ExpoConfig} from '@expo/config'; - -export default ({config}: ConfigContext): ExpoConfig => { - const pluginEntries: NonNullable = [ - [ - 'react-native-iap', - { - // IAPKit API key for purchase verification (optional) - // Get your API key from https://kit.openiap.dev - iapkitApiKey: process.env.EXPO_PUBLIC_IAPKIT_API_KEY, - - // iOS Alternative Billing configuration (optional) - // Uncomment and configure for external purchase support - // NOTE: Requires Apple approval and proper provisioning profile - // iosAlternativeBilling: { - // // Required: Countries where external purchases are supported (ISO 3166-1 alpha-2) - // countries: ['kr', 'nl'], - // // countries: ['kr', 'nl', 'de', 'fr', 'it', 'es'], - // - // // Optional: External purchase URLs per country (iOS 15.4+) - // links: { - // kr: 'https://openiap.dev/kr', - // nl: 'https://openiap.dev/nl', - // }, - // // links: { - // // kr: 'https://your-site.com/kr/checkout', - // // nl: 'https://your-site.com/nl/checkout', - // // de: 'https://your-site.com/de/checkout', - // // }, - // - // // Optional: Multiple URLs per country (iOS 17.5+, up to 5) - // // multiLinks: { - // // fr: [ - // // 'https://your-site.com/fr', - // // 'https://your-site.com/global-sale', - // // ], - // // it: ['https://your-site.com/global-sale'], - // // }, - // - // // Optional: Custom link regions (iOS 18.1+) - // // customLinkRegions: ['de', 'fr', 'nl'], - // - // // Optional: Streaming regions for music apps (iOS 18.2+) - // // streamingLinkRegions: ['at', 'de', 'fr', 'nl', 'is', 'no'], - // - // // Enable external purchase link entitlement - // enableExternalPurchaseLink: true, - // - // // Enable streaming entitlement (music apps only) - // // enableExternalPurchaseLinkStreaming: false, - // }, - }, - ], - 'expo-font', - 'expo-web-browser', - 'expo-router', - [ - 'expo-splash-screen', - { - image: './assets/images/splash-icon.png', - imageWidth: 200, - resizeMode: 'contain', - backgroundColor: '#ffffff', - }, - ], - [ - 'expo-build-properties', - { - android: { - kotlinVersion: '2.0.21', - }, - ios: { - deploymentTarget: '15.1', - }, - }, - ], - ]; - - const expoConfig: ExpoConfig = { - ...config, - name: 'expo-iap-example', - slug: 'expo-iap-example', - version: '1.0.0', - orientation: 'portrait', - icon: './assets/images/icon.png', - scheme: 'expo-iap-example', - userInterfaceStyle: 'automatic', - newArchEnabled: true, - ios: { - ...config.ios, - supportsTablet: true, - bundleIdentifier: 'dev.hyo.martie', - }, - android: { - ...config.android, - adaptiveIcon: { - foregroundImage: './assets/images/adaptive-icon.png', - backgroundColor: '#000000', - }, - package: 'dev.hyo.martie', - }, - plugins: pluginEntries, - experiments: { - ...config.experiments, - typedRoutes: true, - }, - }; - - return expoConfig; -}; diff --git a/libraries/react-native-iap/example-expo/app/_layout.tsx b/libraries/react-native-iap/example-expo/app/_layout.tsx deleted file mode 100644 index f8f7b001..00000000 --- a/libraries/react-native-iap/example-expo/app/_layout.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import {Stack} from 'expo-router'; -import {DataModalProvider} from '../contexts/DataModalContext'; - -export default function RootLayout() { - return ( - - - - - - - - - - - - - ); -} diff --git a/libraries/react-native-iap/example-expo/app/all-products.tsx b/libraries/react-native-iap/example-expo/app/all-products.tsx deleted file mode 100644 index 26ba2154..00000000 --- a/libraries/react-native-iap/example-expo/app/all-products.tsx +++ /dev/null @@ -1,334 +0,0 @@ -import {useCallback, useEffect} from 'react'; -import { - View, - Text, - StyleSheet, - TouchableOpacity, - ScrollView, - Alert, - Platform, -} from 'react-native'; -import {router} from 'expo-router'; -import {useIAP, type Product, type ProductSubscription} from 'react-native-iap'; -import {PRODUCT_IDS, SUBSCRIPTION_PRODUCT_IDS} from '../constants/products'; - -export default function AllProductsScreen() { - const { - connected, - products, - subscriptions, - fetchProducts, - requestPurchase, - finishTransaction, - } = useIAP({ - onPurchaseSuccess: async (purchase) => { - console.log('Purchase successful:', purchase); - Alert.alert('Success', `Purchased: ${purchase.productId}`); - - try { - // Determine if the product is consumable - const allProducts = [...(products || []), ...(subscriptions || [])]; - const product = allProducts.find((p) => p.id === purchase.productId); - const isConsumable = - product?.type === 'in-app' && - PRODUCT_IDS.includes(purchase.productId); - - await finishTransaction({ - purchase, - isConsumable, - }); - console.log('Transaction finished'); - } catch (error) { - console.warn('Failed to finish transaction:', error); - } - }, - onPurchaseError: (error) => { - console.error('Purchase error:', error); - Alert.alert('Error', error.message); - }, - }); - - const handleFetchProducts = useCallback(async () => { - try { - console.log('[AllProducts] Fetching products:', PRODUCT_IDS); - await fetchProducts({skus: PRODUCT_IDS, type: 'in-app'}); - console.log( - '[AllProducts] Fetching subscriptions:', - SUBSCRIPTION_PRODUCT_IDS, - ); - await fetchProducts({skus: SUBSCRIPTION_PRODUCT_IDS, type: 'subs'}); - console.log('[AllProducts] All products fetched successfully'); - } catch (error: any) { - console.error('[AllProducts] Failed to fetch products:', error); - Alert.alert('Error', 'Failed to fetch products'); - } - }, [fetchProducts]); - - // Auto-fetch products when connected - useEffect(() => { - if (connected) { - console.log('[AllProducts] Auto-fetching products on connection'); - handleFetchProducts(); - } - }, [connected, handleFetchProducts]); - - const handlePurchase = useCallback( - (product: Product | ProductSubscription, type: 'in-app' | 'subs') => { - if (!connected) { - Alert.alert('Error', 'Not connected to store'); - return; - } - - requestPurchase({ - request: { - ios: { - sku: product.id, - quantity: 1, - }, - android: { - skus: [product.id], - }, - }, - type, - }); - }, - [connected, requestPurchase], - ); - - const renderProduct = ( - product: Product | ProductSubscription, - type: 'in-app' | 'subs', - ) => ( - - - {product.title} - {product.description} - {product.displayPrice} - - {type === 'in-app' ? '🛒 Product' : '🔄 Subscription'} - - - handlePurchase(product, type)} - disabled={!connected} - > - Buy - - - ); - - return ( - - - router.back()} - style={styles.backButton} - > - ← Back - - All Products - - - - Connection: - - {connected ? '✓ Connected' : '✗ Disconnected'} - - - - - Fetch Products - - - - Products ({products.length}) - {products.length === 0 ? ( - - - No products available. Tap "Fetch Products" to load. - - - ) : ( - products.map((product) => renderProduct(product, 'in-app')) - )} - - - - - Subscriptions ({subscriptions.length}) - - {subscriptions.length === 0 ? ( - - - No subscriptions available. Tap "Fetch Products" to load. - - - ) : ( - subscriptions.map((product) => renderProduct(product, 'subs')) - )} - - - - ℹ️ Test Mode - - {Platform.OS === 'ios' - ? 'Use sandbox accounts for testing' - : 'Use test tracks in Google Play Console'} - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - header: { - padding: 20, - backgroundColor: '#fff', - borderBottomWidth: 1, - borderBottomColor: '#e0e0e0', - }, - backButton: { - marginBottom: 10, - }, - backButtonText: { - fontSize: 16, - color: '#007AFF', - }, - title: { - fontSize: 24, - fontWeight: 'bold', - }, - statusCard: { - flexDirection: 'row', - justifyContent: 'space-between', - margin: 16, - padding: 12, - backgroundColor: '#fff', - borderRadius: 8, - }, - statusLabel: { - fontSize: 14, - fontWeight: '500', - }, - statusValue: { - fontSize: 14, - color: '#999', - }, - connected: { - color: '#4CAF50', - }, - fetchButton: { - margin: 16, - marginTop: 0, - padding: 16, - backgroundColor: '#007AFF', - borderRadius: 8, - alignItems: 'center', - }, - fetchButtonDisabled: { - backgroundColor: '#ccc', - }, - fetchButtonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, - section: { - margin: 16, - marginTop: 0, - }, - sectionTitle: { - fontSize: 18, - fontWeight: '600', - marginBottom: 12, - }, - productCard: { - flexDirection: 'row', - padding: 16, - backgroundColor: '#fff', - borderRadius: 8, - marginBottom: 12, - shadowColor: '#000', - shadowOffset: {width: 0, height: 2}, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 2, - }, - productInfo: { - flex: 1, - marginRight: 12, - }, - productTitle: { - fontSize: 16, - fontWeight: '600', - marginBottom: 4, - }, - productDescription: { - fontSize: 14, - color: '#666', - marginBottom: 8, - }, - productPrice: { - fontSize: 18, - fontWeight: 'bold', - color: '#007AFF', - marginBottom: 4, - }, - productType: { - fontSize: 12, - color: '#999', - }, - buyButton: { - justifyContent: 'center', - paddingHorizontal: 20, - paddingVertical: 12, - backgroundColor: '#007AFF', - borderRadius: 8, - }, - buyButtonDisabled: { - backgroundColor: '#ccc', - }, - buyButtonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, - emptyState: { - padding: 24, - backgroundColor: '#fff', - borderRadius: 8, - alignItems: 'center', - }, - emptyText: { - fontSize: 14, - color: '#999', - textAlign: 'center', - }, - infoCard: { - margin: 16, - marginTop: 0, - padding: 16, - backgroundColor: '#fff3cd', - borderRadius: 8, - borderLeftWidth: 4, - borderLeftColor: '#ffc107', - }, - infoTitle: { - fontSize: 14, - fontWeight: '600', - marginBottom: 4, - }, - infoText: { - fontSize: 12, - color: '#856404', - }, -}); diff --git a/libraries/react-native-iap/example-expo/app/alternative-billing.tsx b/libraries/react-native-iap/example-expo/app/alternative-billing.tsx deleted file mode 100644 index f84f2c31..00000000 --- a/libraries/react-native-iap/example-expo/app/alternative-billing.tsx +++ /dev/null @@ -1,533 +0,0 @@ -import {useCallback, useState, useEffect} from 'react'; -import { - View, - Text, - StyleSheet, - TouchableOpacity, - Alert, - Platform, - TextInput, - ScrollView, -} from 'react-native'; -import {router} from 'expo-router'; -import { - useIAP, - requestPurchase, - initConnection, - endConnection, - presentExternalPurchaseLinkIOS, - isBillingProgramAvailableAndroid, - createBillingProgramReportingDetailsAndroid, - launchExternalLinkAndroid, - type Product, - type Purchase, - type BillingProgramAndroid, -} from 'react-native-iap'; -import {CONSUMABLE_PRODUCT_IDS} from '../constants/products'; - -type AndroidBillingMode = 'billing-programs' | 'external-payments'; - -export default function AlternativeBillingScreen() { - const [externalUrl, setExternalUrl] = useState('https://openiap.dev'); - const [billingMode, setBillingMode] = - useState('billing-programs'); - const [billingProgram, setBillingProgram] = - useState('external-offer'); - const [purchaseResult, setPurchaseResult] = useState(''); - const [isProcessing, setIsProcessing] = useState(false); - const [isReconnecting, setIsReconnecting] = useState(false); - - const { - connected, - products, - fetchProducts, - } = useIAP({ - enableBillingProgramAndroid: - Platform.OS === 'android' ? billingProgram : undefined, - onPurchaseSuccess: async (purchase: Purchase) => { - console.log('Purchase successful:', purchase); - setIsProcessing(false); - setPurchaseResult(`✅ Purchase successful: ${purchase.productId}`); - Alert.alert('Success', 'Purchase completed!'); - }, - onPurchaseError: (error) => { - console.log('[AlternativeBilling] Purchase error:', error); - setIsProcessing(false); - setPurchaseResult(`❌ Error: ${error.message}`); - }, - }); - - const handleFetchProducts = useCallback(async () => { - try { - console.log( - '[AlternativeBilling] Fetching products:', - CONSUMABLE_PRODUCT_IDS, - ); - await fetchProducts({skus: CONSUMABLE_PRODUCT_IDS, type: 'in-app'}); - console.log('[AlternativeBilling] Products fetched successfully'); - } catch (error) { - console.error('[AlternativeBilling] Failed to fetch products:', error); - Alert.alert('Error', 'Failed to fetch products'); - } - }, [fetchProducts]); - - // Auto-fetch products when connected - useEffect(() => { - if (connected) { - console.log('[AlternativeBilling] Auto-fetching products on connection'); - handleFetchProducts(); - } - }, [connected, handleFetchProducts]); - - useEffect(() => { - console.log( - '[AlternativeBilling] Products updated:', - products.length, - products, - ); - }, [products]); - - useEffect(() => { - console.log('[AlternativeBilling] Connected:', connected); - }, [connected]); - - const reconnectWithBillingProgram = useCallback( - async (newProgram: BillingProgramAndroid) => { - try { - setIsReconnecting(true); - setPurchaseResult('Reconnecting with new billing program...'); - await endConnection(); - await new Promise((resolve) => { - setTimeout(() => resolve(), 500); - }); - await initConnection( - Platform.OS === 'android' - ? {enableBillingProgramAndroid: newProgram} - : undefined, - ); - setPurchaseResult(`✅ Reconnected with ${newProgram} program`); - await fetchProducts({skus: CONSUMABLE_PRODUCT_IDS, type: 'in-app'}); - } catch (error: any) { - console.error('[AlternativeBilling] Reconnection error:', error); - setPurchaseResult(`❌ Reconnection failed: ${error.message}`); - } finally { - setIsReconnecting(false); - } - }, - [fetchProducts], - ); - - const handleIOSPurchase = useCallback( - async (product: Product) => { - console.log('[iOS] Starting alternative billing purchase:', product.id); - console.log('[iOS] External URL:', externalUrl); - - if (!externalUrl || externalUrl.trim() === '') { - Alert.alert('Error', 'Please enter a valid external purchase URL'); - return; - } - - setIsProcessing(true); - setPurchaseResult('🌐 Opening external purchase link...'); - - try { - // Use StoreKit External Purchase Link API - const result = await presentExternalPurchaseLinkIOS(externalUrl); - console.log('[iOS] External purchase link result:', result); - - if (result.error) { - setPurchaseResult(`❌ Error: ${result.error}`); - Alert.alert('Error', result.error); - } else if (result.success) { - setPurchaseResult( - `✅ External purchase link opened\n\nProduct: ${product.id}\nURL: ${externalUrl}\n\nUser redirected to external website.`, - ); - Alert.alert( - 'Redirected', - 'User was redirected to your external purchase website.', - ); - } - } catch (error: any) { - console.error('[iOS] Alternative billing error:', error); - setPurchaseResult(`❌ Error: ${error.message}`); - Alert.alert('Error', error.message); - } finally { - setIsProcessing(false); - } - }, - [externalUrl], - ); - - const handleAndroidBillingPrograms = useCallback(async () => { - setIsProcessing(true); - setPurchaseResult('Checking billing program availability...'); - - try { - const result = await isBillingProgramAvailableAndroid(billingProgram); - - if (!result.isAvailable) { - setPurchaseResult( - `❌ Billing program "${billingProgram}" not available`, - ); - Alert.alert('Not Available', `${billingProgram} is not available`); - setIsProcessing(false); - return; - } - - setPurchaseResult('Launching external link...'); - const success = await launchExternalLinkAndroid({ - billingProgram, - launchMode: 'launch-in-external-browser-or-app', - linkType: 'link-to-digital-content-offer', - linkUri: externalUrl, - }); - - if (!success) { - setPurchaseResult('❌ Failed to launch external link'); - Alert.alert('Error', 'Failed to launch external link'); - return; - } - - setPurchaseResult('Creating reporting token...'); - const details = - await createBillingProgramReportingDetailsAndroid(billingProgram); - - setPurchaseResult( - `✅ Billing Programs API completed\n\nProgram: ${billingProgram}\nURL: ${externalUrl}\nToken: ${details.externalTransactionToken.substring(0, 30)}...\n\n⚠️ Report token to Google Play within 24h`, - ); - Alert.alert( - 'Success', - 'External link launched and reporting token created.', - ); - } catch (error: any) { - console.error('[Android] Billing Programs error:', error); - setPurchaseResult(`❌ Error: ${error.message}`); - Alert.alert('Error', error.message); - } finally { - setIsProcessing(false); - } - }, [billingProgram, externalUrl]); - - const handleAndroidExternalPayments = useCallback( - async (product: Product) => { - setIsProcessing(true); - setPurchaseResult('Starting External Payments purchase...'); - - try { - await requestPurchase({ - request: { - android: { - skus: [product.id], - developerBillingOption: { - billingProgram: 'external-payments', - linkUri: externalUrl, - launchMode: 'launch-in-external-browser-or-app', - }, - }, - }, - type: 'in-app', - }); - setPurchaseResult( - `🔄 External Payments dialog shown\n\nProduct: ${product.id}\n\nGoogle Play purchases use purchase callbacks; developer billing uses an external transaction token.`, - ); - } catch (error: any) { - console.error('[Android] External Payments error:', error); - setPurchaseResult(`❌ Error: ${error.message}`); - Alert.alert('Error', error.message); - } finally { - setIsProcessing(false); - } - }, - [externalUrl], - ); - - const handlePurchase = (product: Product) => { - if (Platform.OS === 'ios') { - handleIOSPurchase(product); - } else if (billingMode === 'billing-programs') { - handleAndroidBillingPrograms(); - } else { - handleAndroidExternalPayments(product); - } - }; - - return ( - - - router.back()} - style={styles.backButton} - > - ← Back - - Alternative Billing - - - - - {Platform.OS === 'ios' - ? 'iOS External Purchase' - : 'Android Billing Programs'} - - - {Platform.OS === 'ios' - ? 'Redirects to external website for payment' - : billingMode === 'billing-programs' - ? '3-step flow: availability → external link → reporting token' - : 'External Payments developer billing option'} - - - - - Connection: - - {connected ? '✓ Connected' : '✗ Disconnected'} - - - - {Platform.OS === 'android' && ( - - Billing Mode: - - setBillingMode( - billingMode === 'billing-programs' - ? 'external-payments' - : 'billing-programs', - ) - } - > - - {billingMode === 'billing-programs' - ? 'Billing Programs' - : 'External Payments'} - - - - )} - - {Platform.OS === 'android' && billingMode === 'billing-programs' && ( - - Billing Program: - { - const nextProgram: BillingProgramAndroid = - billingProgram === 'external-offer' - ? 'user-choice-billing' - : billingProgram === 'user-choice-billing' - ? 'external-payments' - : 'external-offer'; - setBillingProgram(nextProgram); - reconnectWithBillingProgram(nextProgram); - }} - disabled={isReconnecting} - > - {billingProgram} - - - )} - - - External URL: - - - - - Fetch Products - - - - Products ({products.length}) - {products.map((product) => ( - handlePurchase(product)} - disabled={isProcessing || isReconnecting || !connected} - > - - {product.title} - {product.displayPrice} - - Buy - - ))} - - - {purchaseResult ? ( - - Result: - {purchaseResult} - - ) : null} - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - header: { - padding: 20, - backgroundColor: '#fff', - borderBottomWidth: 1, - borderBottomColor: '#e0e0e0', - }, - backButton: { - marginBottom: 10, - }, - backButtonText: { - fontSize: 16, - color: '#007AFF', - }, - title: { - fontSize: 24, - fontWeight: 'bold', - }, - infoCard: { - margin: 16, - padding: 16, - backgroundColor: '#e3f2fd', - borderRadius: 8, - }, - infoTitle: { - fontSize: 16, - fontWeight: '600', - marginBottom: 8, - }, - infoText: { - fontSize: 14, - color: '#666', - }, - statusCard: { - flexDirection: 'row', - justifyContent: 'space-between', - margin: 16, - marginTop: 0, - padding: 12, - backgroundColor: '#fff', - borderRadius: 8, - }, - statusLabel: { - fontSize: 14, - fontWeight: '500', - }, - statusValue: { - fontSize: 14, - color: '#999', - }, - connected: { - color: '#4CAF50', - }, - modeSelector: { - margin: 16, - marginTop: 0, - }, - label: { - fontSize: 14, - fontWeight: '500', - marginBottom: 8, - }, - modeButton: { - padding: 12, - backgroundColor: '#fff', - borderRadius: 8, - alignItems: 'center', - }, - modeButtonText: { - fontSize: 16, - fontWeight: '500', - }, - urlInput: { - margin: 16, - marginTop: 0, - }, - input: { - padding: 12, - backgroundColor: '#fff', - borderRadius: 8, - borderWidth: 1, - borderColor: '#e0e0e0', - }, - button: { - margin: 16, - marginTop: 0, - padding: 16, - backgroundColor: '#007AFF', - borderRadius: 8, - alignItems: 'center', - }, - buttonDisabled: { - backgroundColor: '#ccc', - }, - buttonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, - productsSection: { - margin: 16, - marginTop: 0, - }, - sectionTitle: { - fontSize: 18, - fontWeight: '600', - marginBottom: 12, - }, - productCard: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - padding: 16, - backgroundColor: '#fff', - borderRadius: 8, - marginBottom: 8, - }, - productInfo: { - flex: 1, - }, - productTitle: { - fontSize: 16, - fontWeight: '500', - marginBottom: 4, - }, - productPrice: { - fontSize: 14, - color: '#666', - }, - buyButton: { - color: '#007AFF', - fontSize: 16, - fontWeight: '600', - }, - resultCard: { - margin: 16, - marginTop: 0, - padding: 16, - backgroundColor: '#fff', - borderRadius: 8, - }, - resultTitle: { - fontSize: 16, - fontWeight: '600', - marginBottom: 8, - }, - resultText: { - fontSize: 14, - fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace', - }, -}); diff --git a/libraries/react-native-iap/example-expo/app/available-purchases.tsx b/libraries/react-native-iap/example-expo/app/available-purchases.tsx deleted file mode 100644 index 7985acbb..00000000 --- a/libraries/react-native-iap/example-expo/app/available-purchases.tsx +++ /dev/null @@ -1,519 +0,0 @@ -// Generated from example/screens/AvailablePurchases.tsx -// This file is automatically copied during postinstall -// Do not edit directly - modify the source file instead - -import {useState, useEffect, useCallback} from 'react'; -import { - View, - Text, - StyleSheet, - ScrollView, - TouchableOpacity, - ActivityIndicator, - Alert, - Platform, -} from 'react-native'; -import type {PurchaseError} from 'react-native-iap'; -import {useIAP, deepLinkToSubscriptions} from 'react-native-iap'; -import {useDataModal} from '../contexts/DataModalContext'; - -// Define subscription IDs at component level like in the working example -const subscriptionIds = [ - 'dev.hyo.martie.premium', // Same as subscription-flow -]; - -export default function AvailablePurchases() { - const [loading, setLoading] = useState(false); - const [isCheckingStatus, setIsCheckingStatus] = useState(false); - - // Use global modal context - const {showData} = useDataModal(); - - // Use the useIAP hook like subscription-flow does - const { - connected, - subscriptions, - availablePurchases, - activeSubscriptions, - getAvailablePurchases, - getActiveSubscriptions, - fetchProducts, - finishTransaction, - } = useIAP({ - onPurchaseSuccess: async (purchase) => { - console.log('[AVAILABLE-PURCHASES] Purchase successful:', purchase); - - // Finish transaction like in subscription-flow - await finishTransaction({ - purchase, - isConsumable: false, - }); - - // Refresh status after success - setTimeout(() => { - checkSubscriptionStatus(); - }, 1000); - }, - onPurchaseError: (error: PurchaseError) => { - console.error('[AVAILABLE-PURCHASES] Purchase failed:', error); - Alert.alert('Purchase Failed', error.message); - }, - }); - - // Check subscription status like subscription-flow does - const checkSubscriptionStatus = useCallback(async () => { - if (!connected || isCheckingStatus) { - console.log( - '[AVAILABLE-PURCHASES] Skipping subscription status check - not connected or already checking', - ); - return; - } - - console.log('[AVAILABLE-PURCHASES] Checking subscription status...'); - setIsCheckingStatus(true); - try { - const subs = await getActiveSubscriptions(); - console.log('[AVAILABLE-PURCHASES] Active subscriptions result:', subs); - } catch (error) { - console.error( - '[AVAILABLE-PURCHASES] Error checking subscription status:', - error, - ); - console.warn( - '[AVAILABLE-PURCHASES] Subscription status check failed, but existing state preserved', - ); - } finally { - setIsCheckingStatus(false); - } - }, [connected, getActiveSubscriptions, isCheckingStatus]); - - const handleGetAvailablePurchases = async () => { - if (!connected) return; - - setLoading(true); - try { - console.log('Loading available purchases...'); - await getAvailablePurchases(); - console.log('Available purchases request sent'); - } catch (error) { - console.error('Error getting available purchases:', error); - Alert.alert('Error', 'Failed to get available purchases'); - } finally { - setLoading(false); - } - }; - - // Load products and available purchases when connected - follow subscription-flow pattern - useEffect(() => { - if (connected) { - console.log( - '[AVAILABLE-PURCHASES] Connected to store, loading subscription products...', - ); - // Request products first - this is event-based, not promise-based - fetchProducts({skus: subscriptionIds, type: 'subs'}); - console.log( - '[AVAILABLE-PURCHASES] Product loading request sent - waiting for results...', - ); - - // Then load available purchases - console.log('[AVAILABLE-PURCHASES] Loading available purchases...'); - getAvailablePurchases().catch((error) => { - console.warn( - '[AVAILABLE-PURCHASES] Failed to load available purchases:', - error, - ); - }); - } - }, [connected, fetchProducts, getAvailablePurchases]); - - // Check subscription status separately like subscription-flow does - useEffect(() => { - if (connected) { - // Use a timeout to avoid rapid consecutive calls - const timer = setTimeout(() => { - checkSubscriptionStatus(); - }, 500); - - return () => clearTimeout(timer); - } - return undefined; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [connected]); - - // Track state changes for debugging - useEffect(() => { - console.log( - '[AVAILABLE-PURCHASES] availablePurchases:', - availablePurchases.length, - 'items', - ); - }, [availablePurchases]); - - useEffect(() => { - console.log( - '[AVAILABLE-PURCHASES] activeSubscriptions:', - activeSubscriptions.length, - activeSubscriptions, - ); - }, [activeSubscriptions]); - - useEffect(() => { - console.log( - '[AVAILABLE-PURCHASES] subscriptions (products):', - subscriptions.length, - subscriptions, - ); - }, [subscriptions]); - - const openManageSubscriptions = () => { - deepLinkToSubscriptions().catch(() => {}); - }; - - return ( - - - - Store Connection: {connected ? '✅ Connected' : '❌ Disconnected'} - - - - {/* Active Subscriptions Section */} - {activeSubscriptions.length > 0 && ( - - 🔄 Active Subscriptions - - Currently active subscription services - - - {activeSubscriptions.map((subscription, index) => ( - - - {subscription.productId} - - ✅ Active - - - - - {subscription.expirationDateIOS && ( - - Expires: - - {new Date( - subscription.expirationDateIOS, - ).toLocaleDateString()} - {subscription.willExpireSoon && ' (Soon)'} - - - )} - - {subscription.environmentIOS && ( - - Environment: - - {subscription.environmentIOS} - - - )} - - {subscription.daysUntilExpirationIOS != null && ( - - Days Left: - - {subscription.daysUntilExpirationIOS} days - - - )} - - - ))} - - 👤 Manage Subscriptions - - - )} - - {/* Available Purchases Section */} - - 📋 Purchase History - - Past purchases and subscription transactions - - - {availablePurchases.length === 0 && activeSubscriptions.length === 0 ? ( - No purchase history found - ) : availablePurchases.length === 0 ? ( - - No historical purchases found (active subscriptions shown above) - - ) : ( - availablePurchases.map((purchase, index) => ( - { - showData(purchase, `Purchase: ${purchase.productId}`); - }} - > - - Product ID: - {purchase.productId} - - - Platform: - {purchase.platform} - - {purchase.transactionDate && ( - - Date: - - {new Date(purchase.transactionDate).toLocaleDateString()} - - - )} - {purchase.id && ( - - Transaction ID: - {purchase.id} - - )} - - {/* iOS-specific fields with new IOS naming convention */} - {Platform.OS === 'ios' && - 'expirationDateIOS' in purchase && - purchase.expirationDateIOS && ( - - Expires: - - {new Date( - purchase.expirationDateIOS, - ).toLocaleDateString()} - {purchase.expirationDateIOS < Date.now() - ? ' (Expired)' - : ''} - - - )} - - {Platform.OS === 'ios' && - 'environmentIOS' in purchase && - purchase.environmentIOS && ( - - Environment: - {purchase.environmentIOS} - - )} - - {Platform.OS === 'ios' && - 'originalTransactionDateIOS' in purchase && - purchase.originalTransactionDateIOS && ( - - Original Date: - - {new Date( - purchase.originalTransactionDateIOS, - ).toLocaleDateString()} - - - )} - - {/* Android-specific fields */} - {Platform.OS === 'android' && - 'autoRenewingAndroid' in purchase && - purchase.autoRenewingAndroid !== undefined && ( - - Auto Renewing: - - {purchase.autoRenewingAndroid ? '✅ Yes' : '❌ No'} - - - )} - - )) - )} - - - - {loading ? ( - - ) : ( - 🔄 Refresh Purchases - )} - - - ); -} - -const styles = StyleSheet.create({ - container: { - flexGrow: 1, - padding: 20, - backgroundColor: '#f5f5f5', - }, - statusContainer: { - backgroundColor: '#fff', - padding: 15, - borderRadius: 8, - marginBottom: 20, - shadowColor: '#000', - shadowOffset: {width: 0, height: 1}, - shadowOpacity: 0.05, - shadowRadius: 2, - elevation: 2, - }, - statusText: { - fontSize: 16, - fontWeight: '500', - }, - section: { - backgroundColor: '#fff', - padding: 15, - borderRadius: 8, - marginBottom: 20, - shadowColor: '#000', - shadowOffset: {width: 0, height: 1}, - shadowOpacity: 0.05, - shadowRadius: 2, - elevation: 2, - }, - sectionTitle: { - fontSize: 18, - fontWeight: '600', - marginBottom: 8, - }, - subtitle: { - fontSize: 14, - color: '#666', - marginBottom: 15, - fontStyle: 'italic', - }, - purchaseHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 12, - paddingBottom: 8, - borderBottomWidth: 1, - borderBottomColor: '#f0f0f0', - }, - productId: { - fontSize: 16, - fontWeight: '600', - color: '#333', - flex: 1, - }, - statusBadge: { - backgroundColor: '#e8f4f8', - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 12, - }, - statusBadgeText: { - fontSize: 12, - fontWeight: '600', - color: '#007AFF', - }, - purchaseDetails: { - gap: 8, - }, - activeSubscriptionItem: { - borderLeftColor: '#28a745', - backgroundColor: '#f8fff9', - borderLeftWidth: 4, - }, - emptyText: { - color: '#666', - fontStyle: 'italic', - textAlign: 'center', - paddingVertical: 20, - }, - purchaseItem: { - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - marginBottom: 12, - shadowColor: '#000', - shadowOffset: {width: 0, height: 2}, - shadowOpacity: 0.05, - shadowRadius: 4, - elevation: 2, - borderLeftWidth: 4, - borderLeftColor: '#007AFF', - }, - purchaseRow: { - flexDirection: 'row', - marginBottom: 8, - }, - label: { - fontWeight: '500', - width: 120, - color: '#333', - }, - value: { - flex: 1, - color: '#666', - }, - activeText: { - color: '#28a745', - fontWeight: '600', - }, - expiredText: { - color: '#dc3545', - fontWeight: '600', - }, - button: { - backgroundColor: '#007AFF', - paddingHorizontal: 24, - paddingVertical: 16, - borderRadius: 12, - alignItems: 'center', - shadowColor: '#000', - shadowOffset: {width: 0, height: 2}, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, - }, - buttonDisabled: { - backgroundColor: '#ccc', - }, - buttonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, -}); diff --git a/libraries/react-native-iap/example-expo/app/index.tsx b/libraries/react-native-iap/example-expo/app/index.tsx deleted file mode 100644 index 78e61408..00000000 --- a/libraries/react-native-iap/example-expo/app/index.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import {useEffect, useState} from 'react'; -import {View, Text, StyleSheet, TouchableOpacity} from 'react-native'; -import {Link} from 'expo-router'; -import {getStorefront} from 'react-native-iap'; - -/** - * Example App Landing Page - * - * Navigation to focused purchase flow implementations. - * This demonstrates TypeScript-first, platform-agnostic approaches to in-app purchases. - */ -export default function Home() { - const [storefront, setStorefront] = useState(null); - - useEffect(() => { - // getStorefront handles platform checks internally - getStorefront() - .then((storefront: any) => { - setStorefront(storefront); - }) - .catch((error: any) => { - // Silently fail on non-iOS platforms - console.log('Storefront not available:', error.message); - }); - }, []); - - return ( - - expo-iap Examples - - Best Practice Implementations{' '} - {storefront ? `(Store: ${storefront})` : ''} - - - - These examples demonstrate TypeScript-first approaches to in-app - purchases with: - {'\n'}• Automatic type inference (no manual casting) - {'\n'}• Platform-agnostic property access - {'\n'}• Clean error handling with proper types - {'\n'}• Focused implementations for each use case - {'\n'}• CPK React Native compliant code style - - - - - - 🛒 In-App Purchase Flow - One-time products - - - - - - 🔄 Subscription Flow - Recurring subscriptions - - - - - - 📦 Available Purchases - View past purchases - - - - - - 🎁 Offer Code Redemption - Redeem promo codes - - - - - - 💳 Alternative Billing - External payment options - - - - - - 🛍️ All Products - Browse all available items - - - - - - 📡 Webhook Stream - - IAPKit SSE + test notification - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: '#f5f5f5', - padding: 20, - }, - title: { - fontSize: 28, - fontWeight: 'bold', - color: '#333', - marginBottom: 8, - }, - subtitle: { - fontSize: 18, - color: '#666', - marginBottom: 24, - }, - description: { - fontSize: 16, - color: '#555', - textAlign: 'center', - lineHeight: 24, - marginBottom: 32, - paddingHorizontal: 20, - }, - buttonContainer: { - width: '100%', - gap: 16, - }, - button: { - paddingHorizontal: 24, - paddingVertical: 16, - borderRadius: 12, - shadowColor: '#000', - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, - alignItems: 'center', - }, - primaryButton: { - backgroundColor: '#007AFF', - }, - secondaryButton: { - backgroundColor: '#28a745', - }, - tertiaryButton: { - backgroundColor: '#6c757d', - }, - quaternaryButton: { - backgroundColor: '#9c27b0', - }, - alternativeBillingButton: { - backgroundColor: '#ff9800', - }, - allProductsButton: { - backgroundColor: '#00bcd4', - }, - webhookStreamButton: { - backgroundColor: '#0EA5E9', - }, - buttonText: { - color: 'black', - fontSize: 16, - fontWeight: '600', - marginBottom: 4, - }, - buttonSubtext: { - color: 'gray', - fontSize: 14, - }, -}); diff --git a/libraries/react-native-iap/example-expo/app/offer-code.tsx b/libraries/react-native-iap/example-expo/app/offer-code.tsx deleted file mode 100644 index 0a103ecd..00000000 --- a/libraries/react-native-iap/example-expo/app/offer-code.tsx +++ /dev/null @@ -1,325 +0,0 @@ -// Generated from example/screens/OfferCode.tsx -// This file is automatically copied during postinstall -// Do not edit directly - modify the source file instead - -import {useState} from 'react'; -import { - View, - Text, - StyleSheet, - TouchableOpacity, - Alert, - Platform, - ScrollView, - ActivityIndicator, -} from 'react-native'; -import {presentCodeRedemptionSheetIOS, useIAP} from 'react-native-iap'; - -/** - * Offer Code Redemption Example - * - * This example demonstrates how to implement offer code redemption - * functionality for both iOS and Android platforms. - */ - -// Platform-specific content helpers -const getPlatformContent = () => { - const isIOS = Platform.OS === 'ios'; - return { - buttonText: isIOS ? '🎁 Redeem Offer Code' : '🎁 Open Play Store', - buttonSubtext: isIOS ? 'Enter code in-app' : 'Redeem in Play Store', - howItWorks: isIOS - ? '• Tap the button below to open the redemption sheet\n• Enter your offer code\n• The system will validate and apply the code\n• Your purchase will appear in purchase history' - : '• Tap the button to open Google Play Store\n• Enter your promo code in the Play Store\n• Complete the redemption process\n• Return to this app to see your purchase', - platformNote: isIOS - ? 'iOS supports in-app code redemption via StoreKit' - : 'Android requires redemption through Google Play Store', - testingInfo: isIOS - ? '• Use TestFlight or App Store Connect to generate test codes\n• Test on real devices (not simulators)\n• Sandbox environment supports offer codes' - : '• Generate promo codes in Google Play Console\n• Test with your Google account\n• Ensure app is properly configured for IAP', - }; -}; - -export default function OfferCodeScreen() { - const {connected} = useIAP(); - const [isRedeeming, setIsRedeeming] = useState(false); - const platformContent = getPlatformContent(); - const isIOS = Platform.OS === 'ios'; - - const handleRedeemCode = async () => { - if (!connected) { - Alert.alert('Not Connected', 'Please wait for store connection'); - return; - } - - setIsRedeeming(true); - - try { - if (isIOS) { - // Present native iOS redemption sheet - await presentCodeRedemptionSheetIOS(); - Alert.alert( - 'Redemption Sheet Presented', - 'After successful redemption, the purchase will appear in your purchase history.', - ); - } else { - // For Android, we need to guide users to the Play Store - Alert.alert( - 'Android Offer Codes', - 'On Android, offer codes must be redeemed through the Google Play Store.\n\n' + - 'Steps:\n' + - '1. Open Google Play Store\n' + - '2. Tap profile icon → Payments & subscriptions\n' + - '3. Select "Redeem code"\n' + - '4. Enter your promo code\n' + - '5. Return to this app', - [{text: 'OK', style: 'default'}], - ); - } - } catch (error) { - console.error('Error redeeming code:', error); - Alert.alert( - 'Error', - `Failed to redeem code: ${error instanceof Error ? error.message : 'Unknown error'}`, - ); - } finally { - setIsRedeeming(false); - } - }; - - return ( - - - Offer Code Redemption - - - How it works: - {platformContent.howItWorks} - - - - {isRedeeming ? ( - - ) : ( - <> - - {platformContent.buttonText} - - - {platformContent.buttonSubtext} - - - )} - - - - - Platform: {isIOS ? 'iOS' : 'Android'} - - {platformContent.platformNote} - - - - Testing Offer Codes - {platformContent.testingInfo} - - - - Connection Status - - - - {connected ? 'Connected to Store' : 'Connecting...'} - - - - - {!isIOS && ( - - ⚠️ Android Note - - React Native IAP does not have a direct API for opening the Play - Store redemption screen. Users need to manually navigate to the - Play Store to redeem their codes. - - - )} - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - content: { - padding: 20, - }, - title: { - fontSize: 24, - fontWeight: 'bold', - marginBottom: 20, - textAlign: 'center', - color: '#333', - }, - infoCard: { - backgroundColor: 'white', - padding: 16, - borderRadius: 12, - marginBottom: 20, - shadowColor: '#000', - shadowOffset: {width: 0, height: 2}, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, - }, - infoTitle: { - fontSize: 18, - fontWeight: '600', - marginBottom: 8, - color: '#333', - }, - infoText: { - fontSize: 16, - lineHeight: 24, - color: '#555', - }, - redeemButton: { - backgroundColor: '#007AFF', - paddingHorizontal: 24, - paddingVertical: 16, - borderRadius: 12, - alignItems: 'center', - marginBottom: 20, - shadowColor: '#000', - shadowOffset: {width: 0, height: 2}, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, - }, - androidButton: { - backgroundColor: '#4CAF50', - }, - disabledButton: { - opacity: 0.6, - }, - buttonText: { - color: 'white', - fontSize: 18, - fontWeight: '600', - marginBottom: 4, - }, - buttonSubtext: { - color: 'rgba(255, 255, 255, 0.8)', - fontSize: 14, - }, - platformNote: { - backgroundColor: '#e9ecef', - padding: 16, - borderRadius: 12, - marginBottom: 20, - }, - noteTitle: { - fontSize: 16, - fontWeight: '600', - marginBottom: 4, - color: '#495057', - textTransform: 'uppercase', - }, - noteText: { - fontSize: 14, - color: '#6c757d', - }, - testingSection: { - backgroundColor: 'white', - padding: 16, - borderRadius: 12, - marginBottom: 20, - shadowColor: '#000', - shadowOffset: {width: 0, height: 2}, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, - }, - sectionTitle: { - fontSize: 18, - fontWeight: '600', - marginBottom: 8, - color: '#333', - }, - testingText: { - fontSize: 14, - lineHeight: 22, - color: '#555', - }, - statusSection: { - backgroundColor: 'white', - padding: 16, - borderRadius: 12, - marginBottom: 20, - shadowColor: '#000', - shadowOffset: {width: 0, height: 2}, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, - }, - statusTitle: { - fontSize: 16, - fontWeight: '600', - marginBottom: 8, - color: '#333', - }, - statusRow: { - flexDirection: 'row', - alignItems: 'center', - }, - statusIndicator: { - width: 12, - height: 12, - borderRadius: 6, - marginRight: 8, - }, - connected: { - backgroundColor: '#28a745', - }, - disconnected: { - backgroundColor: '#dc3545', - }, - statusText: { - fontSize: 14, - color: '#555', - }, - androidNote: { - backgroundColor: '#fff3cd', - padding: 16, - borderRadius: 12, - borderWidth: 1, - borderColor: '#ffc107', - }, - androidNoteTitle: { - fontSize: 16, - fontWeight: '600', - marginBottom: 8, - color: '#856404', - }, - androidNoteText: { - fontSize: 14, - lineHeight: 20, - color: '#856404', - }, -}); diff --git a/libraries/react-native-iap/example-expo/app/purchase-flow.tsx b/libraries/react-native-iap/example-expo/app/purchase-flow.tsx deleted file mode 100644 index d9d44bad..00000000 --- a/libraries/react-native-iap/example-expo/app/purchase-flow.tsx +++ /dev/null @@ -1,1314 +0,0 @@ -// Generated from example/screens/PurchaseFlow.tsx -// This file is automatically copied during postinstall -// Do not edit directly - modify the source file instead - -import {useCallback, useEffect, useRef, useState} from 'react'; -import { - View, - Text, - StyleSheet, - TouchableOpacity, - Alert, - Platform, - Modal, - ScrollView, -} from 'react-native'; -import Clipboard from '@react-native-clipboard/clipboard'; -import { - requestPurchase, - useIAP, - getAppTransactionIOS, - getStorefront, - ErrorCode, -} from 'react-native-iap'; -// IAPKit API Key - Set this in your environment or replace with your actual key -const IAPKIT_API_KEY = process.env.EXPO_PUBLIC_IAPKIT_API_KEY || ''; -import Loading from '../components/Loading'; -import { - CONSUMABLE_PRODUCT_IDS, - NON_CONSUMABLE_PRODUCT_IDS, - PRODUCT_IDS, -} from '../constants/products'; -import {getErrorMessage} from '../utils/errorUtils'; -import { - useVerificationMethod, - type VerificationMethod, -} from '../hooks/useVerificationMethod'; -import type { - Product, - Purchase, - PurchaseError, - VerifyPurchaseWithProviderProps, -} from 'react-native-iap'; -import PurchaseSummaryRow from '../components/PurchaseSummaryRow'; - -const CONSUMABLE_PRODUCT_ID_SET = new Set(CONSUMABLE_PRODUCT_IDS); -const NON_CONSUMABLE_PRODUCT_ID_SET = new Set(NON_CONSUMABLE_PRODUCT_IDS); - -type PurchaseFlowProps = { - connected: boolean; - products: Product[]; - purchaseResult: string; - isProcessing: boolean; - lastPurchase: Purchase | null; - storefront: string | null; - isFetchingStorefront: boolean; - verificationMethod: VerificationMethod; - onPurchase: (productId: string) => void; - onRefreshStorefront: () => void; - onChangeVerificationMethod: () => void; -}; - -/** - * Purchase Flow Example - In-App Products - * - * Demonstrates useIAP hook approach for in-app products: - * - Uses useIAP hook for purchase management - * - Handles purchase callbacks with proper types - * - No manual promise handling required - * - Clean success/error pattern through hooks - * - Focused on one-time purchases (products) - */ - -function PurchaseFlow({ - connected, - products, - purchaseResult, - isProcessing, - lastPurchase, - storefront, - isFetchingStorefront, - verificationMethod, - onPurchase, - onRefreshStorefront, - onChangeVerificationMethod, -}: PurchaseFlowProps) { - const [selectedProduct, setSelectedProduct] = useState(null); - const [modalVisible, setModalVisible] = useState(false); - - const visibleProducts = products; - const hasHiddenNonConsumables = false; - - const handlePurchase = useCallback( - (itemId: string) => { - onPurchase(itemId); - }, - [onPurchase], - ); - - const handleCopyResult = () => { - if (purchaseResult) { - Clipboard.setString(purchaseResult); - Alert.alert('Copied', 'Purchase result copied to clipboard'); - } - }; - - const checkAppTransaction = async () => { - try { - console.log('Checking app transaction...'); - const transaction = await getAppTransactionIOS(); - - if (transaction) { - Alert.alert( - 'App Transaction', - `App Transaction Found:\n\n` + - `Original App Version: ${ - transaction.originalAppVersion || 'N/A' - }\n` + - `Purchase Date: ${ - transaction.originalPurchaseDate - ? new Date( - transaction.originalPurchaseDate, - ).toLocaleDateString() - : 'N/A' - }\n` + - `Device Verification: ${ - transaction.deviceVerification || 'N/A' - }\n` + - `Environment: ${transaction.environment || 'N/A'}`, - [{text: 'OK'}], - ); - } else { - Alert.alert('App Transaction', 'No app transaction found'); - } - } catch (error) { - console.error('Failed to get app transaction:', error); - Alert.alert('Error', 'Failed to get app transaction'); - } - }; - - const handleShowDetails = (product: Product) => { - setSelectedProduct(product); - setModalVisible(true); - }; - - if (!connected) { - return ; - } - - return ( - - - In-App Purchase Flow - - Testing consumable and non-consumable products - - - - - - - Store Connection: - - {connected ? '✅ Connected' : '❌ Disconnected'} - - - - - Storefront: - - {storefront && storefront.trim().length > 0 - ? storefront - : 'Unavailable'} - - - - - - {isFetchingStorefront - ? 'Fetching storefront...' - : 'Refresh storefront'} - - - - - {/* Verification Method Selector */} - - Purchase Verification: - - - {verificationMethod === 'ignore' - ? '❌ None (Skip)' - : verificationMethod === 'local' - ? '📱 Local (Device)' - : '☁️ IAPKit (Server)'} - - Tap to change - - - - - Available Products - - {visibleProducts.length > 0 - ? `${visibleProducts.length} product(s) available` - : hasHiddenNonConsumables - ? 'All non-consumable products already purchased' - : 'Loading products...'} - - - {visibleProducts.map((product) => ( - - - {product.title} - {product.displayPrice} - - - {product.description} - - - {CONSUMABLE_PRODUCT_ID_SET.has(product.id) - ? 'Consumable product' - : NON_CONSUMABLE_PRODUCT_ID_SET.has(product.id) - ? 'Non-consumable product' - : 'In-app product'} - - - handlePurchase(product.id)} - disabled={isProcessing} - > - - {isProcessing ? 'Processing...' : `Purchase`} - - - handleShowDetails(product)} - > - Details - - - - ))} - - {visibleProducts.length === 0 && connected && ( - - - {hasHiddenNonConsumables - ? 'All available non-consumable products have already been purchased.' - : 'No products available. Please check your app store configuration.'} - - - )} - - - {purchaseResult || lastPurchase ? ( - - {purchaseResult ? ( - <> - Latest Status - {purchaseResult} - - ) : null} - {lastPurchase ? ( - - Latest Purchase - {}} - /> - - ) : null} - {purchaseResult ? ( - - 📋 Copy Message - - ) : null} - - ) : null} - - {Platform.OS === 'ios' && ( - - - 🔍 Check App Transaction (iOS 16+) - - - )} - - - How to test: - - 1. Make sure you're signed in with a Sandbox account - - - 2. Products must be configured in App Store Connect - - - 3. Tap "Purchase" to initiate the transaction - - - 4. The transaction will be processed via the hook callbacks - - - 5. Server-side receipt validation is recommended for production - - - - - setModalVisible(false)} - > - - - Product Details - - {selectedProduct && ( - <> - Product ID: - {selectedProduct.id} - - Title: - {selectedProduct.title} - - Description: - - {selectedProduct.description} - - - Price: - - {selectedProduct.displayPrice} - - - Currency: - - {selectedProduct.currency || 'N/A'} - - - Type: - - {selectedProduct.type || 'N/A'} - - - {'isFamilyShareableIOS' in selectedProduct && ( - <> - - Is Family Shareable: - - - {selectedProduct.isFamilyShareableIOS ? 'Yes' : 'No'} - - - )} - - {/* Discount Offers (Cross-platform) */} - {'discountOffers' in selectedProduct && - selectedProduct.discountOffers && - Array.isArray(selectedProduct.discountOffers) && - selectedProduct.discountOffers.length > 0 && ( - - - Discount Offers ( - {selectedProduct.discountOffers.length}) - - {selectedProduct.discountOffers.map((offer, idx) => ( - - - {offer.id || `Offer ${idx + 1}`} - - Price: - - {offer.displayPrice} - - {offer.fullPriceMicrosAndroid && ( - <> - - Full Price (micros): - - - {offer.fullPriceMicrosAndroid} - - - )} - {offer.percentageDiscountAndroid && ( - - {offer.percentageDiscountAndroid}% off - - )} - {offer.formattedDiscountAmountAndroid && ( - <> - Discount: - - {offer.formattedDiscountAmountAndroid} - - - )} - {offer.validTimeWindowAndroid && ( - <> - - Valid Window: - - - {new Date( - Number( - offer.validTimeWindowAndroid - .startTimeMillis, - ), - ).toLocaleDateString()}{' '} - -{' '} - {new Date( - Number( - offer.validTimeWindowAndroid - .endTimeMillis, - ), - ).toLocaleDateString()} - - - )} - {offer.limitedQuantityInfoAndroid && ( - <> - - Limited Quantity: - - - { - offer.limitedQuantityInfoAndroid - .remainingQuantity - }{' '} - /{' '} - { - offer.limitedQuantityInfoAndroid - .maximumQuantity - }{' '} - remaining - - - )} - {offer.preorderDetailsAndroid && ( - <> - - Pre-order Release: - - - {new Date( - Number( - offer.preorderDetailsAndroid - .preorderReleaseTimeMillis, - ), - ).toLocaleDateString()} - - - )} - {offer.rentalDetailsAndroid && ( - <> - Rental: - - Period:{' '} - { - offer.rentalDetailsAndroid - .rentalExpirationPeriod - } - - - )} - {Array.isArray(offer.offerTagsAndroid) && - offer.offerTagsAndroid.length > 0 && ( - <> - Tags: - - {offer.offerTagsAndroid.join(', ')} - - - )} - - ))} - - )} - - )} - - setModalVisible(false)} - > - Close - - - - - - ); -} - -/** - * ============================================================================ - * Purchase Flow Container - * ============================================================================ - * - * This component demonstrates the complete IAP purchase flow with 6 key steps: - * - * 1. INIT CONNECTION - * - useIAP hook automatically handles connection via initConnection() - * - `connected` state indicates when store is ready - * - * 2. SUBSCRIBE TO EVENTS - * - useIAP internally subscribes to purchase events - * - onPurchaseSuccess: Called when purchase completes successfully - * - onPurchaseError: Called when purchase fails or is cancelled - * - * 3. REQUEST PURCHASE (3 options) - * Option A: iOS-specific request with quantity - * Option B: Android-specific request with SKU array - * Option C: Cross-platform using `request` object (recommended) - * - * 4. VERIFY PURCHASE - * - Local verification: Direct API call to Apple/Google - * - IAPKit verification: Server-side verification via IAPKit service - * - Skip verification: For testing only (not recommended for production) - * - * 5. GRANT ENTITLEMENT - * - Update your backend/database with purchase info - * - Unlock content or features for the user - * - (Handled by your app's business logic) - * - * 6. FINISH TRANSACTION - * - Call finishTransaction() to acknowledge the purchase - * - For consumables: isConsumable: true (allows re-purchase) - * - For non-consumables: isConsumable: false - * - CRITICAL: Always finish transactions to prevent issues - * - * ============================================================================ - */ -function PurchaseFlowContainer() { - // ────────────────────────────────────────────────────────────────────────── - // State - // ────────────────────────────────────────────────────────────────────────── - const [purchaseResult, setPurchaseResult] = useState(''); - const [isProcessing, setIsProcessing] = useState(false); - const [lastPurchase, setLastPurchase] = useState(null); - const [storefront, setStorefront] = useState(null); - const [fetchingStorefront, setFetchingStorefront] = useState(false); - - const { - verificationMethod, - verificationMethodRef, - showVerificationMethodSelector, - } = useVerificationMethod('ignore'); - - // ────────────────────────────────────────────────────────────────────────── - // Step 1: INIT CONNECTION - // Step 2: SUBSCRIBE TO EVENTS - // ────────────────────────────────────────────────────────────────────────── - // useIAP hook automatically: - // - Calls initConnection() on mount - // - Sets up purchase event listeners - // - Cleans up on unmount - const { - connected, - products, - fetchProducts, - finishTransaction, - verifyPurchase, - verifyPurchaseWithProvider, - } = useIAP({ - // ──────────────────────────────────────────────────────────────────────── - // Step 2a: Purchase Success Handler - // ──────────────────────────────────────────────────────────────────────── - onPurchaseSuccess: async (purchase: Purchase) => { - const {purchaseToken: tokenToMask, ...rest} = purchase; - const masked = { - ...rest, - ...(tokenToMask ? {purchaseToken: 'hidden'} : {}), - }; - console.log('Purchase successful:', masked); - console.log('[PurchaseFlow] purchaseState:', purchase.purchaseState); - setLastPurchase(purchase); - setIsProcessing(false); - - setPurchaseResult( - `Purchase completed successfully (state: ${purchase.purchaseState}).`, - ); - - const productId = purchase.productId ?? ''; - const isConsumablePurchase = CONSUMABLE_PRODUCT_ID_SET.has(productId); - if (!isConsumablePurchase && productId) { - if (NON_CONSUMABLE_PRODUCT_ID_SET.has(productId)) { - console.log( - '[PurchaseFlow] Non-consumable purchase recorded:', - productId, - ); - } else { - console.warn( - '[PurchaseFlow] Purchase for product not listed in constants:', - productId, - ); - } - } - - // ────────────────────────────────────────────────────────────────────── - // Step 4: VERIFY PURCHASE - // ────────────────────────────────────────────────────────────────────── - // Choose verification method based on user selection: - // - 'ignore': Skip verification (testing only) - // - 'local': Direct API verification with Apple/Google - // - 'iapkit': Server-side verification via IAPKit - const currentVerificationMethod = verificationMethodRef.current; - console.log('[PurchaseFlow] About to verify purchase:', { - verificationMethod: currentVerificationMethod, - productId, - willVerify: currentVerificationMethod !== 'ignore' && !!productId, - }); - - if (currentVerificationMethod !== 'ignore' && productId) { - setIsProcessing(true); - try { - // ── Option A: Local Verification ────────────────────────────────── - if (currentVerificationMethod === 'local') { - console.log('[PurchaseFlow] Verifying with local method...'); - // Platform-specific verification API - // Provide all platform options - library handles platform detection - // - // ⚠️ SECURITY WARNING: The accessToken below is a PLACEHOLDER. - // NEVER ship OAuth tokens directly in your app bundle! - // In production, your mobile app should: - // 1. Send the purchaseToken to YOUR backend server - // 2. Your backend authenticates with Google Play Developer API - // 3. Your backend returns the verification result to the app - // This example uses a placeholder for demonstration purposes only. - const result = await verifyPurchase({ - apple: {sku: productId}, - google: { - sku: productId, - // PLACEHOLDER - Replace with token fetched from your backend - accessToken: 'YOUR_OAUTH_ACCESS_TOKEN', - packageName: 'dev.hyo.martie', - purchaseToken: purchase.purchaseToken ?? '', - isSub: false, - }, - // horizon: { sku: productId, userId: '...', accessToken: '...' } - }); - console.log('[PurchaseFlow] Local verification result:', result); - } - // ── Option B: IAPKit Server Verification ────────────────────────── - else if (currentVerificationMethod === 'iapkit') { - console.log('[PurchaseFlow] Verifying with IAPKit...'); - // NOTE: Set your API key in .env file as IAPKIT_API_KEY - const apiKey = IAPKIT_API_KEY; - - console.log( - '[PurchaseFlow] API Key loaded:', - apiKey ? '✓ Present' : '✗ Missing', - ); - console.log( - '[PurchaseFlow] purchase.purchaseToken:', - purchase.purchaseToken - ? `✓ Present (${purchase.purchaseToken.length} chars)` - : '✗ Missing or empty', - ); - - if (!apiKey) { - throw new Error('IAPKIT_API_KEY not configured'); - } - - const jwsOrToken = purchase.purchaseToken ?? ''; - if (!jwsOrToken) { - console.warn( - '[PurchaseFlow] No purchaseToken/JWS available for verification', - ); - throw new Error( - 'No purchase token available for IAPKit verification', - ); - } - - const verifyRequest: VerifyPurchaseWithProviderProps = { - provider: 'iapkit', - iapkit: { - apiKey, - apple: { - jws: jwsOrToken, - }, - google: { - purchaseToken: jwsOrToken, - }, - }, - }; - - console.log( - '[PurchaseFlow] Sending IAPKit verification request:', - JSON.stringify( - { - provider: verifyRequest.provider, - iapkit: { - apiKey: '***hidden***', - ...(Platform.OS === 'ios' - ? {apple: {jws: `${jwsOrToken.substring(0, 50)}...`}} - : { - google: { - purchaseToken: `${jwsOrToken.substring(0, 50)}...`, - }, - }), - }, - }, - null, - 2, - ), - ); - - const result = await verifyPurchaseWithProvider(verifyRequest); - console.log('[PurchaseFlow] IAPKit verification result:', result); - - // Show verification result to user - if (result.iapkit) { - const statusEmoji = result.iapkit.isValid ? '✅' : '⚠️'; - const stateText = result.iapkit.state || 'unknown'; - - Alert.alert( - `${statusEmoji} IAPKit Verification`, - `Valid: ${result.iapkit.isValid}\nState: ${stateText}\nStore: ${ - result.iapkit.store || 'unknown' - }`, - ); - } else if (result.errors && result.errors.length > 0) { - const errorMessages = result.errors - .map((e) => `${e.code ? `[${e.code}] ` : ''}${e.message}`) - .join('\n'); - Alert.alert('⚠️ IAPKit Verification Error', errorMessages); - } - } - } catch (error) { - console.warn('[PurchaseFlow] Verification failed:', error); - Alert.alert( - 'Verification Failed', - `Purchase verification failed: ${getErrorMessage(error)}`, - ); - } finally { - setIsProcessing(false); - } - } - - // ────────────────────────────────────────────────────────────────────── - // Step 5: GRANT ENTITLEMENT - // ────────────────────────────────────────────────────────────────────── - // TODO: In production, update your backend here: - // - Save purchase record to database - // - Unlock premium features for user - // - Update user's subscription status - // Example: await yourBackend.grantEntitlement(purchase); - - // ────────────────────────────────────────────────────────────────────── - // Step 6: FINISH TRANSACTION - // ────────────────────────────────────────────────────────────────────── - // CRITICAL: Always finish transactions! - // - Consumables: Set isConsumable: true to allow re-purchase - // - Non-consumables: Set isConsumable: false - // - Failing to finish will cause issues on next app launch - try { - await finishTransaction({ - purchase, - isConsumable: isConsumablePurchase, - }); - } catch (error) { - console.warn('[PurchaseFlow] finishTransaction failed:', error); - } - - Alert.alert('Success', 'Purchase completed successfully!'); - }, - - // ──────────────────────────────────────────────────────────────────────── - // Step 2b: Purchase Error Handler - // ──────────────────────────────────────────────────────────────────────── - onPurchaseError: (error: PurchaseError) => { - console.error('Purchase failed:', error); - console.error('Error code:', error.code); - console.error( - 'Is user cancelled:', - error.code === ErrorCode.UserCancelled, - ); - - setIsProcessing(false); - - // Check for user cancellation - don't show error for this - if (error.code === ErrorCode.UserCancelled) { - setPurchaseResult('Purchase cancelled by user'); - return; - } - - setPurchaseResult( - `Purchase failed: ${error.message} (code: ${error.code})`, - ); - }, - }); - - // ────────────────────────────────────────────────────────────────────────── - // Helpers - // ────────────────────────────────────────────────────────────────────────── - const didFetchRef = useRef(false); - - const fetchStorefront = useCallback(async () => { - setFetchingStorefront(true); - try { - const code = await getStorefront(); - setStorefront(code?.trim() ? code : null); - } catch (error) { - console.warn('[PurchaseFlow] getStorefront failed:', error); - setStorefront(null); - } finally { - setFetchingStorefront(false); - } - }, []); - - // ────────────────────────────────────────────────────────────────────────── - // Fetch Products on Connection - // ────────────────────────────────────────────────────────────────────────── - useEffect(() => { - console.log('[PurchaseFlow] useEffect - connected:', connected); - console.log('[PurchaseFlow] PRODUCT_IDS:', PRODUCT_IDS); - if (connected && !didFetchRef.current) { - didFetchRef.current = true; - console.log('[PurchaseFlow] Calling fetchProducts with:', PRODUCT_IDS); - fetchProducts({skus: PRODUCT_IDS, type: 'in-app'}) - .then(() => { - console.log('[PurchaseFlow] fetchProducts completed'); - }) - .catch((error) => { - console.error('[PurchaseFlow] fetchProducts error:', error); - }); - - void fetchStorefront(); - } else if (!connected) { - didFetchRef.current = false; - console.log('[PurchaseFlow] Not fetching products - not connected'); - setStorefront(null); - } - }, [connected, fetchProducts, fetchStorefront]); - - // ────────────────────────────────────────────────────────────────────────── - // Step 3: REQUEST PURCHASE - // ────────────────────────────────────────────────────────────────────────── - // Three options for requesting purchases: - // - // Option A - iOS only: - // requestPurchase({ request: { ios: { sku, quantity } }, type: 'in-app' }) - // - // Option B - Android only: - // requestPurchase({ request: { android: { skus: [sku] } }, type: 'in-app' }) - // - // Option C - Cross-platform (recommended): - // requestPurchase({ - // request: { - // ios: { sku, quantity: 1 }, - // android: { skus: [sku] } - // }, - // type: 'in-app' - // }) - const handlePurchase = useCallback((itemId: string) => { - setIsProcessing(true); - setPurchaseResult('Processing purchase...'); - - if (typeof requestPurchase !== 'function') { - console.warn('[PurchaseFlow] requestPurchase missing (test/mock env)'); - setIsProcessing(false); - setPurchaseResult('Cannot start purchase in test/mock environment.'); - return; - } - - // Using Option C: Cross-platform request - void requestPurchase({ - request: { - ios: { - sku: itemId, - quantity: 1, - }, - android: { - skus: [itemId], - }, - }, - type: 'in-app', - }); - }, []); - - const handleRefreshStorefront = useCallback(() => { - void fetchStorefront(); - }, [fetchStorefront]); - - return ( - - ); -} - -export default PurchaseFlowContainer; - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - header: { - backgroundColor: '#007AFF', - padding: 20, - paddingTop: 40, - }, - title: { - fontSize: 24, - fontWeight: 'bold', - color: 'white', - marginBottom: 5, - }, - subtitle: { - fontSize: 14, - color: 'rgba(255, 255, 255, 0.8)', - }, - content: { - padding: 15, - }, - statusContainer: { - backgroundColor: 'white', - padding: 15, - borderRadius: 8, - marginBottom: 15, - gap: 12, - }, - statusRow: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - statusLabel: { - fontSize: 14, - color: '#666', - marginRight: 10, - }, - statusValue: { - fontSize: 14, - fontWeight: '600', - }, - statusActionButton: { - alignSelf: 'flex-start', - backgroundColor: '#007AFF', - paddingVertical: 8, - paddingHorizontal: 14, - borderRadius: 6, - }, - statusActionButtonText: { - color: 'white', - fontSize: 13, - fontWeight: '600', - }, - verificationContainer: { - backgroundColor: 'white', - borderRadius: 8, - padding: 15, - marginBottom: 15, - }, - verificationButton: { - backgroundColor: '#f0f0f0', - borderRadius: 6, - padding: 12, - marginTop: 8, - borderWidth: 1, - borderColor: '#e0e0e0', - }, - verificationButtonText: { - fontSize: 16, - fontWeight: '600', - color: '#333', - marginBottom: 4, - }, - verificationButtonHint: { - fontSize: 12, - color: '#666', - fontStyle: 'italic', - }, - section: { - marginBottom: 20, - }, - sectionTitle: { - fontSize: 18, - fontWeight: '600', - marginBottom: 5, - }, - sectionSubtitle: { - fontSize: 12, - color: '#666', - marginBottom: 10, - }, - productCard: { - backgroundColor: 'white', - borderRadius: 8, - padding: 15, - marginBottom: 10, - shadowColor: '#000', - shadowOffset: {width: 0, height: 1}, - shadowOpacity: 0.1, - shadowRadius: 2, - elevation: 2, - }, - productHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 8, - }, - productTitle: { - fontSize: 16, - fontWeight: '600', - flex: 1, - }, - productPrice: { - fontSize: 16, - fontWeight: 'bold', - color: '#007AFF', - }, - productDescription: { - fontSize: 14, - color: '#666', - marginBottom: 12, - }, - productBadgeText: { - fontSize: 12, - fontWeight: '600', - letterSpacing: 0.5, - marginBottom: 12, - textTransform: 'uppercase', - }, - productBadgeConsumable: { - color: '#43A047', - }, - productBadgeNonConsumable: { - color: '#6A1B9A', - }, - productActions: { - flexDirection: 'row', - gap: 10, - }, - purchaseButton: { - flex: 1, - backgroundColor: '#007AFF', - paddingVertical: 10, - borderRadius: 6, - alignItems: 'center', - }, - purchaseButtonText: { - color: 'white', - fontWeight: '600', - fontSize: 14, - }, - detailsButton: { - paddingVertical: 10, - paddingHorizontal: 20, - borderRadius: 6, - borderWidth: 1, - borderColor: '#007AFF', - alignItems: 'center', - }, - detailsButtonText: { - color: '#007AFF', - fontWeight: '600', - fontSize: 14, - }, - emptyState: { - backgroundColor: 'white', - borderRadius: 8, - padding: 20, - alignItems: 'center', - }, - emptyStateText: { - fontSize: 14, - color: '#666', - textAlign: 'center', - }, - resultContainer: { - backgroundColor: '#e8f5e9', - borderRadius: 8, - padding: 15, - marginBottom: 15, - }, - resultTitle: { - fontSize: 14, - fontWeight: '600', - marginBottom: 8, - }, - resultSubtitle: { - fontSize: 13, - fontWeight: '600', - marginBottom: 4, - }, - resultText: { - fontSize: 12, - color: '#333', - fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace', - }, - copyButton: { - paddingVertical: 10, - paddingHorizontal: 20, - backgroundColor: '#4CAF50', - borderRadius: 6, - borderWidth: 1, - borderColor: '#4CAF50', - minHeight: 44, - justifyContent: 'center', - marginTop: 12, - }, - copyButtonText: { - color: 'white', - fontSize: 14, - fontWeight: '600', - }, - refreshButton: { - marginTop: 8, - paddingVertical: 10, - borderRadius: 6, - borderWidth: 1, - borderColor: '#007AFF', - alignItems: 'center', - backgroundColor: 'white', - }, - refreshButtonText: { - color: '#007AFF', - fontWeight: '600', - fontSize: 14, - }, - appTransactionButton: { - backgroundColor: '#FF9800', - paddingVertical: 12, - paddingHorizontal: 20, - borderRadius: 6, - alignItems: 'center', - marginBottom: 15, - }, - appTransactionButtonText: { - color: 'white', - fontWeight: '600', - fontSize: 14, - }, - instructions: { - backgroundColor: '#fff3e0', - borderRadius: 8, - padding: 15, - }, - instructionsTitle: { - fontSize: 14, - fontWeight: '600', - marginBottom: 10, - color: '#e65100', - }, - instructionsText: { - fontSize: 12, - color: '#666', - marginBottom: 5, - }, - modalOverlay: { - flex: 1, - backgroundColor: 'rgba(0, 0, 0, 0.5)', - justifyContent: 'center', - alignItems: 'center', - }, - modalContent: { - backgroundColor: 'white', - borderRadius: 12, - padding: 20, - width: '90%', - maxHeight: '80%', - }, - modalTitle: { - fontSize: 18, - fontWeight: 'bold', - marginBottom: 15, - }, - modalHeaderRow: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - marginBottom: 10, - }, - modalCloseIconButton: { - padding: 4, - }, - modalCloseIconText: { - fontSize: 22, - color: '#666', - }, - modalLabel: { - fontSize: 12, - color: '#666', - marginTop: 10, - marginBottom: 5, - }, - modalValue: { - fontSize: 14, - color: '#333', - marginBottom: 5, - }, - purchaseDetailsContainer: { - gap: 10, - }, - purchaseDetailRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - gap: 12, - }, - closeButton: { - backgroundColor: '#007AFF', - paddingVertical: 12, - borderRadius: 8, - alignItems: 'center', - marginTop: 20, - }, - closeButtonText: { - color: 'white', - fontWeight: '600', - fontSize: 16, - }, - modalScrollView: { - maxHeight: '85%', - }, - offersSection: { - marginTop: 20, - paddingTop: 15, - borderTopWidth: 1, - borderTopColor: '#e0e0e0', - }, - offersSectionTitle: { - fontSize: 16, - fontWeight: '700', - color: '#333', - marginBottom: 12, - }, - offerCard: { - backgroundColor: '#f8f9fa', - borderRadius: 8, - padding: 12, - marginBottom: 10, - borderLeftWidth: 3, - borderLeftColor: '#007AFF', - }, - offerTitle: { - fontSize: 14, - fontWeight: '700', - color: '#007AFF', - marginBottom: 8, - }, - offerLabel: { - fontSize: 11, - color: '#666', - marginTop: 6, - fontWeight: '600', - }, - offerValue: { - fontSize: 13, - color: '#333', - marginTop: 2, - }, - offerValueDiscount: { - fontSize: 13, - color: '#E53935', - marginTop: 2, - fontWeight: '600', - }, - offerToken: { - fontSize: 10, - color: '#999', - fontFamily: 'monospace', - }, -}); diff --git a/libraries/react-native-iap/example-expo/app/subscription-flow.tsx b/libraries/react-native-iap/example-expo/app/subscription-flow.tsx deleted file mode 100644 index 56d1cf15..00000000 --- a/libraries/react-native-iap/example-expo/app/subscription-flow.tsx +++ /dev/null @@ -1,2941 +0,0 @@ -// Generated from example/screens/SubscriptionFlow.tsx -// This file is automatically copied during postinstall -// Do not edit directly - modify the source file instead - -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import { - View, - Text, - StyleSheet, - TouchableOpacity, - Alert, - ScrollView, - Platform, - ActivityIndicator, - Modal, -} from 'react-native'; -import Clipboard from '@react-native-clipboard/clipboard'; -import { - requestPurchase, - useIAP, - deepLinkToSubscriptions, - type ActiveSubscription, - type ProductSubscription, - type ProductSubscriptionAndroid, - type Purchase, - type PurchaseError, - type VerifyPurchaseWithProviderProps, - type SubscriptionOffer, - ErrorCode, -} from 'react-native-iap'; -// IAPKit API Key - Set this in your environment or replace with your actual key -const IAPKIT_API_KEY = process.env.EXPO_PUBLIC_IAPKIT_API_KEY || ''; -import Loading from '../components/Loading'; -import {SUBSCRIPTION_PRODUCT_IDS} from '../constants/products'; -import {getErrorMessage} from '../utils/errorUtils'; -import { - useVerificationMethod, - type VerificationMethod, -} from '../hooks/useVerificationMethod'; -import PurchaseSummaryRow from '../components/PurchaseSummaryRow'; - -type ExtendedPurchase = Purchase & { - purchaseTokenAndroid?: string; - dataAndroid?: { - purchaseToken?: string; - }; - purchaseState?: string; - offerToken?: string; -}; - -// Extended type for ActiveSubscription with additional fields that may be present -// but are not officially part of the ActiveSubscription type definition. -// These fields are either: -// - Detected/computed locally (basePlanId, _detectedBasePlanId) -// - Available in the underlying Purchase but not mapped to ActiveSubscription (isUpgradedIOS) -// - Platform-specific fields (purchaseTokenAndroid) -type ExtendedActiveSubscription = ActiveSubscription & { - basePlanId?: string; // Android: detected from subscription offers - purchaseTokenAndroid?: string; // Android: purchase token - _detectedBasePlanId?: string; // Locally detected/cached base plan ID - isUpgradedIOS?: boolean; // iOS: from PurchaseIOS.isUpgradedIOS -}; - -// Component for plan change controls -interface PlanChangeControlsProps { - activeSubscriptions: ActiveSubscription[]; - handlePlanChange: ( - productId: string, - changeType: 'upgrade' | 'downgrade' | 'yearly' | 'monthly', - currentBasePlanId: string, - ) => void; - isProcessing: boolean; - lastPurchasedPlan: string | null; -} - -const PlanChangeControls = React.memo(function PlanChangeControls({ - activeSubscriptions, - handlePlanChange, - isProcessing, - lastPurchasedPlan, -}: PlanChangeControlsProps) { - // Find all premium subscriptions (both monthly and yearly) - const premiumSubs = activeSubscriptions.filter( - (sub) => - sub.productId === 'dev.hyo.martie.premium' || - sub.productId === 'dev.hyo.martie.premium_year', - ); - - if (premiumSubs.length === 0) return null; - - // Detect the current plan based on product ID for iOS - let currentBasePlan = 'unknown'; - let activeSub: ActiveSubscription | undefined = undefined; - - if (Platform.OS === 'ios') { - // On iOS, find the most recent subscription (in case both exist during transition) - // Sort by transaction date to get the most recent one - const sortedSubs = [...premiumSubs].sort((a, b) => { - const dateA = a.transactionDate ?? 0; - const dateB = b.transactionDate ?? 0; - return dateB - dateA; - }); - - activeSub = sortedSubs[0]; - - // Check for the most recent purchase to determine actual plan - // First, check if both products exist (transition state) - const hasYearly = premiumSubs.some( - (s) => s.productId === 'dev.hyo.martie.premium_year', - ); - const hasMonthly = premiumSubs.some( - (s) => s.productId === 'dev.hyo.martie.premium', - ); - - if (lastPurchasedPlan) { - // If we have a recently purchased plan, use that - currentBasePlan = lastPurchasedPlan; - console.log('Using last purchased plan:', lastPurchasedPlan); - } else if (hasYearly && !hasMonthly) { - // Only yearly exists - user has yearly - currentBasePlan = 'premium-year'; - } else if (!hasYearly && hasMonthly) { - // Only monthly exists - user has monthly - currentBasePlan = 'premium'; - } else if (activeSub) { - // Both exist or transition state - use the most recent one - if (activeSub.productId === 'dev.hyo.martie.premium_year') { - currentBasePlan = 'premium-year'; - } else if (activeSub.productId === 'dev.hyo.martie.premium') { - currentBasePlan = 'premium'; - } - } - } else { - // Android uses base plans within the same product - activeSub = premiumSubs[0]; - const extendedSub = activeSub as ExtendedActiveSubscription; - if (extendedSub.basePlanId) { - currentBasePlan = extendedSub.basePlanId; - } else if (lastPurchasedPlan) { - currentBasePlan = lastPurchasedPlan; - } else { - // Default to monthly if we can't detect - currentBasePlan = 'premium'; - } - } - - console.log( - 'Button section - current base plan:', - currentBasePlan, - 'Active sub:', - activeSub?.productId, - ); - - // iOS doesn't need upgrade/downgrade buttons as it's handled automatically by the App Store - if (Platform.OS === 'ios') { - return null; - } - - return ( - - {currentBasePlan === 'premium' && ( - - handlePlanChange( - activeSub?.productId || 'dev.hyo.martie.premium', - 'upgrade', - 'premium', - ) - } - disabled={isProcessing} - > - - ⬆️ Upgrade to Yearly Plan - - - Save with annual billing - - - )} - - {currentBasePlan === 'premium-year' && ( - - handlePlanChange( - activeSub?.productId || 'dev.hyo.martie.premium_year', - 'downgrade', - 'premium-year', - ) - } - disabled={isProcessing} - > - - ⬇️ Downgrade to Monthly Plan - - - More flexibility with monthly billing - - - )} - - ); -}); - -/** - * Subscription Flow Example - Subscription Products - * - * Demonstrates useIAP hook approach for subscriptions: - * - Uses useIAP hook for subscription management - * - Handles subscription callbacks with proper types - * - No manual promise handling required - * - Clean success/error pattern through hooks - * - Focused on recurring subscriptions - * - * New subscription status checking API: - * - getActiveSubscriptions() - gets all active subscriptions automatically - * - getActiveSubscriptions(['id1', 'id2']) - gets specific subscriptions - * - activeSubscriptions state - automatically updated subscription list - */ - -type SubscriptionFlowProps = { - connected: boolean; - subscriptions: ProductSubscription[]; - activeSubscriptions: ActiveSubscription[]; - purchaseResult: string; - isProcessing: boolean; - isCheckingStatus: boolean; - lastPurchase: Purchase | null; - lastPurchasedPlan: string | null; - verificationMethod: VerificationMethod; - setIsProcessing: (value: boolean) => void; - setPurchaseResult: (value: string) => void; - setLastPurchasedPlan: (value: string | null) => void; - onSubscribe: (productId: string) => void; - onRetryLoadSubscriptions: () => void; - onRefreshStatus: () => void; - onManageSubscriptions: () => void; - onChangeVerificationMethod: () => void; -}; - -function SubscriptionFlow({ - connected, - subscriptions, - activeSubscriptions, - purchaseResult, - isProcessing, - isCheckingStatus, - lastPurchase, - lastPurchasedPlan, - verificationMethod, - setIsProcessing, - setPurchaseResult, - onSubscribe, - onRetryLoadSubscriptions, - onRefreshStatus, - onManageSubscriptions, - onChangeVerificationMethod, -}: SubscriptionFlowProps) { - const [selectedSubscription, setSelectedSubscription] = - useState(null); - const [modalVisible, setModalVisible] = useState(false); - - const ownedSubscriptions = useMemo(() => { - return new Set(activeSubscriptions.map((sub) => sub.productId)); - }, [activeSubscriptions]); - - const handleSubscription = useCallback( - (itemId: string) => { - const isAlreadySubscribed = ownedSubscriptions.has(itemId); - - if (isAlreadySubscribed) { - Alert.alert( - 'Already Subscribed', - 'You already have an active subscription to this product.', - [{text: 'OK', style: 'default'}], - ); - return; - } - onSubscribe(itemId); - }, - [onSubscribe, ownedSubscriptions], - ); - - const handleSubscriptionPress = (subscription: ProductSubscription) => { - setSelectedSubscription(subscription); - setModalVisible(true); - }; - - const handlePlanChange = useCallback( - ( - currentProductId: string, - changeType: 'upgrade' | 'downgrade' | 'yearly' | 'monthly', - currentBasePlanId: string, - ) => { - // iOS doesn't use this function anymore as upgrade/downgrade is handled by App Store - if (Platform.OS === 'ios') { - return; - } - - // Android uses the same product with different base plans - const targetProductId = 'dev.hyo.martie.premium'; - - // Find the subscription with the target base plan - const targetSubscription = subscriptions.find( - (s) => s.id === targetProductId, - ); - - if (!targetSubscription) { - Alert.alert('Error', 'Target subscription plan not found'); - return; - } - - // Determine target base plan based on current plan and change type - let targetBasePlanId = ''; - let actionDescription = ''; - - if (currentBasePlanId === 'premium') { - // Currently on monthly, can only upgrade - if (changeType === 'upgrade' || changeType === 'yearly') { - targetBasePlanId = 'premium-year'; - actionDescription = 'upgrade to Yearly'; - } else { - Alert.alert('Info', 'You are already on the Monthly plan'); - return; - } - } else if (currentBasePlanId === 'premium-year') { - // Currently on yearly, can only downgrade - if (changeType === 'downgrade' || changeType === 'monthly') { - targetBasePlanId = 'premium'; - actionDescription = 'downgrade to Monthly'; - } else { - Alert.alert('Info', 'You are already on the Yearly plan'); - return; - } - } else { - // Can't detect current plan, allow switching to either - if (changeType === 'upgrade' || changeType === 'yearly') { - targetBasePlanId = 'premium-year'; - actionDescription = 'switch to Yearly'; - } else if (changeType === 'downgrade' || changeType === 'monthly') { - targetBasePlanId = 'premium'; - actionDescription = 'switch to Monthly'; - } - } - - console.log('Plan change:', { - currentBasePlanId, - targetBasePlanId, - changeType, - }); - - Alert.alert( - 'Change Subscription Plan', - `Do you want to ${actionDescription} plan?`, - [ - { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'Confirm', - onPress: async () => { - setIsProcessing(true); - setPurchaseResult('Processing plan change...'); - - // Get the current subscription to find purchase token - const currentSub = activeSubscriptions.find( - (s) => s.productId === currentProductId, - ); - - if (Platform.OS === 'android') { - // Android subscription replacement - const targetSubWithDetails = - targetSubscription as ProductSubscriptionAndroid; - const androidOffers = targetSubWithDetails.subscriptionOffers; - const targetOffer = androidOffers?.find( - (offer) => offer.basePlanIdAndroid === targetBasePlanId, - ); - - if (!targetOffer) { - Alert.alert('Error', 'Target plan not available'); - setIsProcessing(false); - return; - } - - // For Android, get purchase token from activeSubscriptions - const extendedSub = currentSub as - | ExtendedActiveSubscription - | undefined; - const purchaseToken = - extendedSub?.purchaseToken || - extendedSub?.purchaseTokenAndroid; - - if (!purchaseToken) { - Alert.alert( - 'Error', - 'Unable to find current subscription purchase token. Please try refreshing your subscription status.', - ); - setIsProcessing(false); - return; - } - - // Make sure purchase token is a string - const tokenString = - typeof purchaseToken === 'string' - ? purchaseToken - : String(purchaseToken); - - // Use replacement mode for Android - // ProrationMode constants from Google Play Billing: - // 1 = IMMEDIATE_WITH_TIME_PRORATION - // 2 = IMMEDIATE_AND_CHARGE_PRORATED_PRICE - // 3 = IMMEDIATE_AND_CHARGE_FULL_PRICE - // 4 = DEFERRED - // 5 = IMMEDIATE_WITHOUT_PRORATION - // For same product with different offers, OpenIAP uses CHARGE_FULL_PRICE (5) - const replacementMode = 5; // IMMEDIATE_WITHOUT_PRORATION as per OpenIAP example - - console.log('Plan change params:', { - skus: [targetProductId], - currentBasePlanId, - targetBasePlanId, - offerToken: targetOffer.offerTokenAndroid, - replacementMode, - purchaseToken: tokenString - ? `<${tokenString.substring(0, 10)}...>` - : 'missing', - allOffers: androidOffers?.map((o) => ({ - basePlanId: o.basePlanIdAndroid, - offerId: o.id, - offerToken: o.offerTokenAndroid?.substring(0, 20) + '...', - })), - }); - - // Make the request with proper token - void requestPurchase({ - request: { - google: { - skus: [targetProductId], - subscriptionOffers: [ - { - sku: targetProductId, - offerToken: targetOffer.offerTokenAndroid ?? '', - }, - ], - replacementMode: replacementMode, - purchaseToken: tokenString, - }, - }, - type: 'subs', - }).catch((err: PurchaseError) => { - console.error('Plan change failed:', err); - console.error('Full error:', JSON.stringify(err)); - - // More helpful error messages - let errorMessage = err.message; - if ( - err.message?.includes('DEVELOPER_ERROR') || - err.message?.includes('Invalid arguments') - ) { - errorMessage = - 'Unable to change subscription plan. This may be due to:\n' + - '• Subscriptions not being in the same group in Play Console\n' + - '• Invalid offer configuration\n' + - '• Missing purchase token\n\n' + - 'Original error: ' + - err.message; - } - - setIsProcessing(false); - setPurchaseResult(`❌ Plan change failed: ${err.message}`); - Alert.alert('Plan Change Failed', errorMessage); - }); - } - }, - }, - ], - ); - }, - [subscriptions, activeSubscriptions, setIsProcessing, setPurchaseResult], - ); - - const copyToClipboard = (subscription: ProductSubscription) => { - const jsonString = JSON.stringify(subscription, null, 2); - Clipboard.setString(jsonString); - Alert.alert('Copied', 'Subscription JSON copied to clipboard'); - }; - - const renderSubscriptionDetails = useMemo(() => { - if (!selectedSubscription) return null; - - const jsonString = JSON.stringify(selectedSubscription, null, 2); - - // Check for subscription offers (cross-platform) - const hasSubscriptionOffers = - 'subscriptionOffers' in selectedSubscription && - selectedSubscription.subscriptionOffers && - selectedSubscription.subscriptionOffers.length > 0; - - // Check for discount offers (cross-platform) - const hasDiscountOffers = - 'discountOffers' in selectedSubscription && - selectedSubscription.discountOffers && - selectedSubscription.discountOffers.length > 0; - - return ( - - - {/* Basic Info */} - Product ID: - {selectedSubscription.id} - - Title: - {selectedSubscription.title} - - Price: - - {selectedSubscription.displayPrice} - - - {/* Subscription Offers (Cross-platform) */} - {hasSubscriptionOffers && ( - - - Subscription Offers ( - {selectedSubscription.subscriptionOffers!.length}) - - {selectedSubscription.subscriptionOffers!.map( - (offer: SubscriptionOffer, index: number) => ( - - - {offer.id || `Offer ${index + 1}`} - - - Price: - {offer.displayPrice} - - Type: - {offer.type} - - {offer.paymentMode && ( - <> - - Payment Mode: - - - {offer.paymentMode} - - - )} - - {offer.period && ( - <> - Period: - - {offer.period.value} {offer.period.unit} - - - )} - - {offer.basePlanIdAndroid && ( - <> - - Base Plan ID: - - - {offer.basePlanIdAndroid} - - - )} - - {offer.pricingPhasesAndroid?.pricingPhaseList && - offer.pricingPhasesAndroid.pricingPhaseList.length > - 0 && ( - <> - - Pricing Phases: - - {offer.pricingPhasesAndroid.pricingPhaseList.map( - (phase, phaseIndex) => ( - - - Phase {phaseIndex + 1}: {phase.formattedPrice}{' '} - / {phase.billingPeriod} - - - Cycles: {phase.billingCycleCount}, Mode:{' '} - {phase.recurrenceMode} - - - ), - )} - - )} - - {Array.isArray(offer.offerTagsAndroid) && - offer.offerTagsAndroid.length > 0 && ( - <> - Tags: - - {offer.offerTagsAndroid.join(', ')} - - - )} - - {offer.offerTokenAndroid && ( - <> - - Offer Token: - - - {offer.offerTokenAndroid} - - - )} - - ), - )} - - )} - - {/* Discount Offers (Cross-platform) */} - {hasDiscountOffers && ( - - - Discount Offers ({selectedSubscription.discountOffers!.length}) - - {selectedSubscription.discountOffers!.map((offer, idx) => ( - - - {offer.id || `Offer ${idx + 1}`} - - Price: - {offer.displayPrice} - {offer.fullPriceMicrosAndroid && ( - <> - - Full Price (micros): - - - {offer.fullPriceMicrosAndroid} - - - )} - {offer.percentageDiscountAndroid && ( - - {offer.percentageDiscountAndroid}% off - - )} - {offer.formattedDiscountAmountAndroid && ( - <> - Discount: - - {offer.formattedDiscountAmountAndroid} - - - )} - - ))} - - )} - - {/* Raw JSON Section */} - - Raw JSON: - {jsonString} - - - - copyToClipboard(selectedSubscription)} - > - 📋 Copy - - { - console.log('=== SUBSCRIPTION DATA ==='); - console.log(selectedSubscription); - console.log('=== SUBSCRIPTION JSON ==='); - console.log(jsonString); - Alert.alert('Console', 'Subscription data logged to console'); - }} - > - 🖥️ Console - - - - ); - }, [selectedSubscription]); - - if (!connected) { - return ; - } - - const renderIntroductoryOffer = (subscription: ProductSubscription) => { - if (Platform.OS === 'ios' && 'introductoryPriceIOS' in subscription) { - if (subscription.introductoryPriceIOS) { - const paymentMode = subscription.introductoryPricePaymentModeIOS; - const numberOfPeriods = - subscription.introductoryPriceNumberOfPeriodsIOS; - const subscriptionPeriod = - subscription.introductoryPriceSubscriptionPeriodIOS; - - const periodLabel = subscriptionPeriod - ? subscriptionPeriod.toLowerCase() - : 'period'; - - if (paymentMode === 'free-trial') { - return `${numberOfPeriods} ${periodLabel} free trial`; - } - if (paymentMode === 'pay-as-you-go') { - return `${subscription.introductoryPriceIOS} for ${numberOfPeriods} ${periodLabel}`; - } - if (paymentMode === 'pay-up-front') { - return `${subscription.introductoryPriceIOS} for first ${numberOfPeriods} ${periodLabel}`; - } - } - } - return null; - }; - - const renderSubscriptionPeriod = (subscription: ProductSubscription) => { - if (Platform.OS === 'ios' && 'subscriptionPeriodUnitIOS' in subscription) { - const periodUnit = subscription.subscriptionPeriodUnitIOS; - const periodNumber = subscription.subscriptionPeriodNumberIOS; - if (periodUnit && periodNumber) { - const units: Record = { - day: 'day', - week: 'week', - month: 'month', - year: 'year', - }; - const periodNum = parseInt(periodNumber, 10); - const normalizedUnit = units[periodUnit] || periodUnit; - return `${periodNumber} ${normalizedUnit}${periodNum > 1 ? 's' : ''}`; - } - } - return 'subscription'; - }; - - const renderSubscriptionPrice = (subscription: ProductSubscription) => { - // Use cross-platform subscriptionOffers first - if ( - 'subscriptionOffers' in subscription && - subscription.subscriptionOffers - ) { - const offers = subscription.subscriptionOffers; - if (offers && offers.length > 0) { - const firstOffer = offers[0]; - // Use displayPrice from offer if available - if (firstOffer?.displayPrice) { - return firstOffer.displayPrice; - } - // Fallback to pricingPhasesAndroid - if (firstOffer?.pricingPhasesAndroid?.pricingPhaseList) { - const pricingPhaseList = - firstOffer.pricingPhasesAndroid.pricingPhaseList; - if (pricingPhaseList.length > 0) { - const firstPhase = pricingPhaseList[0]; - if (firstPhase) { - return firstPhase.formattedPrice; - } - } - } - } - return subscription.displayPrice; - } - return subscription.displayPrice; - }; - - return ( - - - Subscription Flow - - React Native IAP Subscription Management with useIAP Hook - - - - Store: {connected ? '✅ Connected' : '❌ Disconnected'} - - - Platform: {Platform.OS === 'ios' ? '🍎 iOS' : '🤖 Android'} - - - - {/* Verification Method Selector */} - - Purchase Verification: - - - {verificationMethod === 'ignore' - ? '❌ None (Skip)' - : verificationMethod === 'local' - ? '📱 Local (Device)' - : '☁️ IAPKit (Server)'} - - Tap to change - - - - - {activeSubscriptions.length > 0 && ( - - Current Subscription Status - - - Status: - - ✅ Active - - - - {(() => { - // For iOS, filter to show only the most recent subscription in the group - let subsToShow = [...activeSubscriptions]; - - if (Platform.OS === 'ios') { - // Filter out duplicates for iOS subscription group - const premiumSubs = activeSubscriptions.filter( - (sub) => - sub.productId === 'dev.hyo.martie.premium' || - sub.productId === 'dev.hyo.martie.premium_year', - ); - - if (premiumSubs.length > 1) { - // Sort by transaction date and keep only the most recent - const sortedPremiumSubs = [...premiumSubs].sort((a, b) => { - const dateA = a.transactionDate ?? 0; - const dateB = b.transactionDate ?? 0; - return dateB - dateA; - }); - - const mostRecentPremium = sortedPremiumSubs[0]; - - // Filter out old premium subscriptions, keep only the most recent - subsToShow = activeSubscriptions.filter((sub) => { - if ( - sub.productId === 'dev.hyo.martie.premium' || - sub.productId === 'dev.hyo.martie.premium_year' - ) { - return sub === mostRecentPremium; - } - return true; // Keep all non-premium subscriptions - }); - } - } - - return subsToShow.map((sub: any, index: number) => { - // Find the matching subscription to get offer details - const matchingSubscription = subscriptions.find( - (s) => s.id === sub.productId, - ); - - // Plan detection for dev.hyo.martie.premium - let activeOfferLabel = ''; - let detectedBasePlanId = ''; - - if ( - (sub.productId === 'dev.hyo.martie.premium' || - sub.productId === 'dev.hyo.martie.premium_year') && - matchingSubscription - ) { - // Log the full data to understand what's available - console.log( - 'ActiveSubscription data:', - JSON.stringify(sub, null, 2), - ); - const extendedSub = sub as ExtendedActiveSubscription; - console.log( - 'Product ID:', - sub.productId, - 'Is Upgraded?:', - extendedSub.isUpgradedIOS, - ); - - if (Platform.OS === 'ios') { - // iOS: Detect based on product ID - if (sub.productId === 'dev.hyo.martie.premium_year') { - detectedBasePlanId = 'premium-year'; - activeOfferLabel = '📅 Yearly Plan'; - } else { - detectedBasePlanId = 'premium'; - activeOfferLabel = '📆 Monthly Plan'; - } - } else { - // Android: Try to detect the base plan from various sources - // Method 1: Check if basePlanId is directly available from native - if (extendedSub.basePlanId) { - detectedBasePlanId = extendedSub.basePlanId; - activeOfferLabel = - detectedBasePlanId === 'premium-year' - ? '📅 Yearly Plan' - : '📆 Monthly Plan'; - } - // Method 2: Check localStorage for last purchased plan - else { - // Try to get from state - const storedPlan = lastPurchasedPlan; - - if (storedPlan === 'premium-year') { - detectedBasePlanId = 'premium-year'; - activeOfferLabel = '📅 Yearly Plan'; - } else { - // Default to monthly - detectedBasePlanId = 'premium'; - activeOfferLabel = '📆 Monthly Plan'; - } - - console.log( - 'Detected plan from state:', - storedPlan || 'none (defaulting to monthly)', - ); - } - } - - // We'll use this detectedBasePlanId in the button section below - } - - // No need for separate handling since we already check both products above - - return ( - - - Product: - {sub.productId} - - - {activeOfferLabel && - (sub.productId === 'dev.hyo.martie.premium' || - sub.productId === 'dev.hyo.martie.premium_year') && ( - - Current Plan: - - {activeOfferLabel} - - - )} - - {sub.expirationDateIOS && ( - - Expires: - - {new Date(sub.expirationDateIOS).toLocaleDateString()} - - - )} - - {Platform.OS === 'android' && - sub.isActive !== undefined && ( - - Auto-Renew: - - {sub.isActive ? '✅ Enabled' : '⚠️ Cancelled'} - - - )} - - {sub.transactionId && ( - - Transaction ID: - - {sub.transactionId.substring(0, 10)}... - - - )} - - {/* 🆕 NEW: renewalInfoIOS showcase */} - {Platform.OS === 'ios' && sub.renewalInfoIOS && ( - - - 📱 iOS Renewal Info (NEW!) - - - {/* Auto-renew status */} - - - Will Auto-Renew: - - - {sub.renewalInfoIOS.willAutoRenew - ? '✅ Yes' - : '⚠️ No'} - - - - {/* Pending upgrade detection */} - {sub.renewalInfoIOS.pendingUpgradeProductId && ( - - - Upgrade Pending: - - - ⬆️ {sub.renewalInfoIOS.pendingUpgradeProductId} - - - )} - - {/* Next renewal date */} - {sub.renewalInfoIOS.renewalDate && ( - - - Next Renewal: - - - {new Date( - sub.renewalInfoIOS.renewalDate, - ).toLocaleDateString()} - - - )} - - {/* Expiration reason */} - {sub.renewalInfoIOS.expirationReason && ( - - - Expiration Reason: - - - {sub.renewalInfoIOS.expirationReason} - - - )} - - {/* Billing retry status */} - {sub.renewalInfoIOS.isInBillingRetry && ( - <> - - - Billing Status: - - - 💳 In Billing Retry - - - {sub.renewalInfoIOS.gracePeriodExpirationDate && ( - - - Grace Period Ends: - - - {new Date( - sub.renewalInfoIOS.gracePeriodExpirationDate, - ).toLocaleDateString()} - - - )} - - )} - - {/* Price increase status */} - {sub.renewalInfoIOS.priceIncreaseStatus && ( - - - Price Increase: - - - {sub.renewalInfoIOS.priceIncreaseStatus} - - - )} - - {/* Auto-renew preference (if different from current product) */} - {sub.renewalInfoIOS.autoRenewPreference && - sub.renewalInfoIOS.autoRenewPreference !== - sub.productId && ( - - - Will Renew As: - - - {sub.renewalInfoIOS.autoRenewPreference} - - - )} - - {/* 🆕 NEW: Renewal offer type */} - {sub.renewalInfoIOS.renewalOfferType && ( - - - 🆕 Offer Type: - - - {sub.renewalInfoIOS.renewalOfferType} - - - )} - - {/* 🆕 NEW: Renewal offer ID */} - {sub.renewalInfoIOS.renewalOfferId && ( - - 🆕 Offer ID: - - {sub.renewalInfoIOS.renewalOfferId} - - - )} - - {/* 🆕 NEW: JSON Representation availability */} - {sub.renewalInfoIOS.jsonRepresentation && ( - - - 🆕 JSON Available: - - - ✅ Yes - - - )} - - )} - - ); - }); - })()} - - - {/* Upgrade/Downgrade button for Android only */} - - - - {isCheckingStatus ? ( - - ) : ( - Check Status - )} - - - )} - - {/* Subscription Upgrade Detection - iOS renewalInfo from ActiveSubscription */} - {Platform.OS === 'ios' && - activeSubscriptions.length > 0 && - activeSubscriptions.some((sub) => { - const pendingProductId = sub.renewalInfoIOS?.pendingUpgradeProductId; - - // Show upgrade card if there's a pending upgrade product that's different - // from the current product. In production, you might want to also check - // willAutoRenew, but Apple Sandbox behavior can be inconsistent. - return pendingProductId && pendingProductId !== sub.productId; - }) ? ( - - - - 🎉 Subscription Upgrade Detected - - - Your subscription will be upgraded at the next renewal - - - {activeSubscriptions.map((sub) => { - const renewalInfo = sub.renewalInfoIOS; - const pendingProductId = renewalInfo?.pendingUpgradeProductId; - - if (!pendingProductId || pendingProductId === sub.productId) { - return null; - } - - const currentProduct = subscriptions.find( - (s) => s.id === sub.productId, - ); - const upgradingToProduct = subscriptions.find( - (s) => s.id === pendingProductId, - ); - - return ( - - - Current Plan - - {currentProduct?.title || sub.productId} - - - - - - - Upgrading To - - {upgradingToProduct?.title || pendingProductId} - - - - {renewalInfo?.willAutoRenew !== undefined ? ( - - Auto-Renew: - - {renewalInfo.willAutoRenew - ? '✅ Enabled' - : '⚠️ Disabled'} - - - ) : null} - - {renewalInfo?.renewalDate ? ( - - Renewal Date: - - {new Date(renewalInfo.renewalDate).toLocaleDateString()} - - - ) : null} - - { - Alert.alert( - 'Renewal Info', - JSON.stringify(renewalInfo, null, 2), - [ - { - text: 'Copy', - onPress: () => - Clipboard.setString( - JSON.stringify(renewalInfo, null, 2), - ), - }, - {text: 'Close'}, - ], - ); - }} - > - - View Full renewalInfo - - - - ); - })} - - - ) : null} - - {/* Subscription Cancellation Detection - iOS renewalInfo from ActiveSubscription */} - {Platform.OS === 'ios' && - activeSubscriptions.length > 0 && - activeSubscriptions.some((sub) => { - const renewalInfo = sub.renewalInfoIOS; - - // Show cancellation card if willAutoRenew is false and no pending upgrade - return ( - renewalInfo?.willAutoRenew === false && - !renewalInfo?.pendingUpgradeProductId - ); - }) ? ( - - - - ⚠️ Subscription Will Not Renew - - - Your subscription is active but will not automatically renew - - - {activeSubscriptions.map((sub) => { - const renewalInfo = sub.renewalInfoIOS; - - if ( - renewalInfo?.willAutoRenew !== false || - renewalInfo?.pendingUpgradeProductId - ) { - return null; - } - - const currentProduct = subscriptions.find( - (s) => s.id === sub.productId, - ); - - return ( - - - - Active Until - - - {currentProduct?.title || sub.productId} - - {renewalInfo?.renewalDate ? ( - - Expires:{' '} - {new Date(renewalInfo.renewalDate).toLocaleDateString()} - - ) : null} - - - { - Alert.alert( - 'Renewal Info', - JSON.stringify(renewalInfo, null, 2), - [ - { - text: 'Copy', - onPress: () => - Clipboard.setString( - JSON.stringify(renewalInfo, null, 2), - ), - }, - {text: 'Close'}, - ], - ); - }} - > - - View Full renewalInfo - - - - ); - })} - - - ) : null} - - - - Available Subscriptions - - Manage - - - - {subscriptions.length > 0 - ? `${subscriptions.length} subscription(s) available` - : 'No subscriptions found. Configure products in the console.'} - - - {subscriptions.length > 0 ? ( - subscriptions.map((subscription) => { - const introOffer = renderIntroductoryOffer(subscription); - const periodLabel = renderSubscriptionPeriod(subscription); - const priceLabel = renderSubscriptionPrice(subscription); - const owned = ownedSubscriptions.has(subscription.id); - - return ( - - - - - {subscription.title} - - - {subscription.description} - - - handleSubscriptionPress(subscription)} - > - ℹ️ - - - - - {priceLabel} - {periodLabel} - - - {introOffer ? ( - - {introOffer} - - ) : null} - - handleSubscription(subscription.id)} - disabled={isProcessing || owned} - > - - {owned - ? 'Already Subscribed' - : isProcessing - ? 'Processing...' - : 'Subscribe'} - - - - ); - }) - ) : ( - - - No subscriptions found. Please configure your products. - - - Retry - - - )} - - - {purchaseResult || lastPurchase ? ( - - Latest Activity - {purchaseResult ? ( - - {purchaseResult} - - ) : null} - {lastPurchase ? ( - - Latest Purchase - {}} /> - - ) : null} - - ) : null} - - setModalVisible(false)} - > - - - - Subscription Details - setModalVisible(false)} - > - - - - {renderSubscriptionDetails} - - - - - - 🔄 Key Features with useIAP Hook - - • Automatic connection handling with purchase callbacks - - - • Active subscription tracking with `getActiveSubscriptions` - - - • Auto-refresh of purchases after successful transactions - - - • Platform-specific offer handling built-in - - - - ); -} - -/** - * ============================================================================ - * Subscription Flow Container - * ============================================================================ - * - * This component demonstrates the complete subscription lifecycle with proper - * handling of platform-specific differences between iOS and Android. - * - * ┌─────────────────────────────────────────────────────────────────────────┐ - * │ PLATFORM COMPARISON - Subscription Data Availability │ - * ├─────────────────────────────┬─────────────┬─────────────┬──────────────┤ - * │ Information │ iOS Client │ Android │ Server │ - * ├─────────────────────────────┼─────────────┼─────────────┼──────────────┤ - * │ Auto-renew status │ ✅ willAutoRenew │ ✅ isAutoRenewing │ ✅ │ - * │ Next renewal product │ ✅ autoRenewPreference │ ❌ │ ✅ │ - * │ Pending upgrade/downgrade │ ✅ pendingUpgradeProductId │ ❌ │ ✅ │ - * │ Expiration reason │ ✅ expirationReason │ ❌ │ ✅ │ - * │ Grace period status │ ✅ gracePeriodExpirationDate │ ❌ │ ✅ │ - * │ Billing retry status │ ✅ isInBillingRetry │ ❌ │ ✅ │ - * │ Renewal date │ ✅ renewalDate │ ❌ │ ✅ │ - * │ Detailed subscription state │ ✅ │ ❌ │ ✅ │ - * └─────────────────────────────┴─────────────┴─────────────┴──────────────┘ - * - * 💡 Key Takeaway: iOS provides rich subscription data client-side via - * renewalInfoIOS, while Android requires server-side calls for details. - * - * ============================================================================ - * SUBSCRIPTION LIFECYCLE FLOWS - * ============================================================================ - * - * 1. ON APP LAUNCH - * ├─ iOS: initConnection → getAvailablePurchases → check transactionState - * │ → validate with server → update entitlements → finishTransaction - * └─ Android: initConnection → getAvailablePurchases → for pending purchases - * → validate → acknowledge → grant entitlements - * - * 2. NEW PURCHASE FLOW - * ├─ iOS: requestPurchase → purchaseUpdatedListener receives PurchaseIOS - * │ → check transactionState (purchased/pending/failed/deferred) - * │ → validate → deliver → finishTransaction - * └─ Android: requestPurchase → purchaseUpdatedListener receives PurchaseAndroid - * → check purchaseState (0=pending, 1=purchased, 2=failed) - * → validate → acknowledge → grant entitlements - * - * 3. CHECKING SUBSCRIPTION STATUS - * ├─ iOS: getActiveSubscriptions → check renewalInfoIOS: - * │ • willAutoRenew = false → show renewal prompt - * │ • isInBillingRetry = true → show payment issue - * │ • pendingUpgradeProductId → show pending change - * └─ Android: getActiveSubscriptions → check isActive - * (detailed info requires server-side API call) - * - * 4. DETECTING CANCELLATIONS - * ├─ iOS: willAutoRenew = false (user still has access until expirationDate) - * └─ Android: isAutoRenewing = false (access until expiry) - * - * 5. HANDLING EXPIRATION - * └─ getActiveSubscriptions returns empty → revoke access → show re-subscribe - * - * 6. RESTORING PURCHASES - * ├─ iOS: getAvailablePurchases → StoreKit fetches from Apple ID history - * │ → validate each → grant access → finishTransaction - * └─ Android: getAvailablePurchases → returns cached purchases - * → validate each → grant access - * - * ============================================================================ - * WHEN TO VALIDATE (Server-side recommended) - * ============================================================================ - * • After purchase — Verify the purchase is legitimate - * • On restore — Check current status (active/cancelled/refunded/expired) - * • Periodically for active subscriptions — Detect refunds and cancellations - * • On app launch — Sync subscription state with server - * - * ⚠️ REFUND EDGE CASE: Refunds bypass client entirely. Server-side validation - * with App Store Server Notifications V2 (iOS) or RTDN (Android) is required. - * - * ============================================================================ - */ -function SubscriptionFlowContainer() { - // ────────────────────────────────────────────────────────────────────────── - // State - // ────────────────────────────────────────────────────────────────────────── - const [isProcessing, setIsProcessing] = useState(false); - const [isCheckingStatus, setIsCheckingStatus] = useState(false); - const [purchaseResult, setPurchaseResult] = useState(''); - const [lastPurchase, setLastPurchase] = useState(null); - const [lastPurchasedPlan, setLastPurchasedPlan] = useState( - null, - ); - - const { - verificationMethod, - verificationMethodRef, - showVerificationMethodSelector, - } = useVerificationMethod('ignore'); - - const lastSuccessAtRef = useRef(0); - const connectedRef = useRef(false); - const fetchedProductsOnceRef = useRef(false); - const statusAutoCheckedRef = useRef(false); - - // ────────────────────────────────────────────────────────────────────────── - // STEP 1: INIT CONNECTION + SUBSCRIBE TO EVENTS - // ────────────────────────────────────────────────────────────────────────── - // useIAP hook automatically: - // - Calls initConnection() on mount - // - Sets up purchase event listeners (onPurchaseSuccess, onPurchaseError) - // - Provides activeSubscriptions state for easy status checking - // - Cleans up on unmount - const { - connected, - subscriptions, - activeSubscriptions, - fetchProducts, - finishTransaction, - getActiveSubscriptions, - verifyPurchase, - verifyPurchaseWithProvider, - } = useIAP({ - // ──────────────────────────────────────────────────────────────────────── - // STEP 2: NEW PURCHASE FLOW - Success Handler - // ──────────────────────────────────────────────────────────────────────── - // Called when purchaseUpdatedListener receives a successful purchase. - // iOS: Check transactionState (purchased/pending/failed/deferred) - // Android: Check purchaseState (0=pending, 1=purchased, 2=failed) - onPurchaseSuccess: async (purchase: Purchase) => { - const {purchaseToken, ...safePurchase} = purchase || {}; - console.log('Purchase successful (redacted):', safePurchase); - - // Try to detect which plan was purchased - if (Platform.OS === 'ios') { - // iOS uses separate products - if (purchase.productId === 'dev.hyo.martie.premium_year') { - setLastPurchasedPlan('premium-year'); - console.log('Detected yearly plan from purchase (iOS)'); - } else if (purchase.productId === 'dev.hyo.martie.premium') { - setLastPurchasedPlan('premium'); - console.log('Detected monthly plan from purchase (iOS)'); - } - } else if (purchase.productId === 'dev.hyo.martie.premium') { - // Android: Check if we have offerToken or other data to identify the plan - const purchaseData = purchase as ExtendedPurchase; - - // Log full purchase data to understand what's available - console.log( - 'Full purchase data for plan detection:', - JSON.stringify(purchaseData, null, 2), - ); - - // Map offerToken to basePlanId using fetched subscription data (cross-platform) - if (purchaseData.offerToken) { - const premiumSub = subscriptions.find( - (s) => s.id === 'dev.hyo.martie.premium', - ) as ProductSubscriptionAndroid; - const matchingOffer = premiumSub?.subscriptionOffers?.find( - (offer) => offer.offerTokenAndroid === purchaseData.offerToken, - ); - if (matchingOffer?.basePlanIdAndroid) { - setLastPurchasedPlan(matchingOffer.basePlanIdAndroid); - console.log( - 'Detected plan from offerToken (Android):', - matchingOffer.basePlanIdAndroid, - ); - } else { - // Fallback if we can't find the matching offer - console.log( - 'Could not map offerToken to basePlanId:', - purchaseData.offerToken, - ); - } - } - } - - lastSuccessAtRef.current = Date.now(); - setLastPurchase(purchase); - setIsProcessing(false); - - setPurchaseResult( - `✅ Subscription activated\n` + - `Product: ${purchase.productId}\n` + - `Transaction ID: ${purchase.id}\n` + - `Date: ${new Date(purchase.transactionDate).toLocaleDateString()}`, - ); - - const productId = purchase.productId ?? ''; - - // ────────────────────────────────────────────────────────────────────── - // STEP 3: VERIFY PURCHASE - // ────────────────────────────────────────────────────────────────────── - // Choose verification method: - // - 'ignore': Skip verification (testing only - NOT for production) - // - 'local': Direct API verification with Apple/Google - // - 'iapkit': Server-side verification via IAPKit (recommended) - // - // ⚠️ Server-side validation is recommended for production: - // - iOS: App Store Server API + App Store Server Notifications V2 - // - Android: Google Play Developer API + RTDN - const currentVerificationMethod = verificationMethodRef.current; - console.log('[SubscriptionFlow] About to verify purchase:', { - verificationMethod: currentVerificationMethod, - productId, - willVerify: currentVerificationMethod !== 'ignore' && !!productId, - }); - - if (currentVerificationMethod !== 'ignore' && productId) { - setIsProcessing(true); - try { - if (currentVerificationMethod === 'local') { - console.log('[SubscriptionFlow] Verifying with local method...'); - // New platform-specific verification API - provide all platform options - // The library internally handles which options to use based on platform - const result = await verifyPurchase({ - apple: {sku: productId}, - google: { - sku: productId, - // NOTE: accessToken must be obtained from your backend server - // that has authenticated with Google Play Developer API - accessToken: 'YOUR_OAUTH_ACCESS_TOKEN', - packageName: 'dev.hyo.martie', - purchaseToken: purchase.purchaseToken ?? '', - isSub: true, - }, - // horizon: { sku: productId, userId: '...', accessToken: '...' } - }); - console.log( - '[SubscriptionFlow] Local verification result:', - result, - ); - } else if (currentVerificationMethod === 'iapkit') { - console.log('[SubscriptionFlow] Verifying with IAPKit...'); - // NOTE: Set your API key in .env file as IAPKIT_API_KEY - const apiKey = IAPKIT_API_KEY; - - console.log( - '[SubscriptionFlow] API Key loaded:', - apiKey ? '✓ Present' : '✗ Missing', - ); - console.log( - '[SubscriptionFlow] purchase.purchaseToken:', - purchase.purchaseToken - ? `✓ Present (${purchase.purchaseToken.length} chars)` - : '✗ Missing or empty', - ); - - if (!apiKey) { - throw new Error('IAPKIT_API_KEY not configured'); - } - - const jwsOrToken = purchase.purchaseToken ?? ''; - if (!jwsOrToken) { - console.warn( - '[SubscriptionFlow] No purchaseToken/JWS available for verification', - ); - throw new Error( - 'No purchase token available for IAPKit verification', - ); - } - - const verifyRequest: VerifyPurchaseWithProviderProps = { - provider: 'iapkit', - iapkit: { - apiKey, - apple: { - jws: jwsOrToken, - }, - google: { - purchaseToken: jwsOrToken, - }, - }, - }; - - console.log( - '[SubscriptionFlow] Sending IAPKit verification request:', - JSON.stringify( - { - provider: verifyRequest.provider, - iapkit: { - apiKey: '***hidden***', - ...(Platform.OS === 'ios' - ? {apple: {jws: `${jwsOrToken.substring(0, 50)}...`}} - : { - google: { - purchaseToken: `${jwsOrToken.substring(0, 50)}...`, - }, - }), - }, - }, - null, - 2, - ), - ); - - const result = await verifyPurchaseWithProvider(verifyRequest); - console.log( - '[SubscriptionFlow] IAPKit verification result:', - result, - ); - - // Show verification result to user - if (result.iapkit) { - const statusEmoji = result.iapkit.isValid ? '✅' : '⚠️'; - const stateText = result.iapkit.state || 'unknown'; - - Alert.alert( - `${statusEmoji} IAPKit Verification`, - `Valid: ${result.iapkit.isValid}\nState: ${stateText}\nStore: ${ - result.iapkit.store || 'unknown' - }`, - ); - } else if (result.errors && result.errors.length > 0) { - const errorMessages = result.errors - .map((e) => `${e.code ? `[${e.code}] ` : ''}${e.message}`) - .join('\n'); - Alert.alert('⚠️ IAPKit Verification Error', errorMessages); - } - } - } catch (error) { - console.warn('[SubscriptionFlow] Verification failed:', error); - Alert.alert( - 'Verification Failed', - `Purchase verification failed: ${getErrorMessage(error)}`, - ); - } finally { - setIsProcessing(false); - } - } - - // ────────────────────────────────────────────────────────────────────── - // STEP 4: GRANT ENTITLEMENT - // ────────────────────────────────────────────────────────────────────── - // TODO: In production, update your backend here: - // - Save subscription record to database - // - Unlock premium features for user - // - Update user's subscription status - // - Handle subscription tiers/levels - // Example: await yourBackend.grantSubscriptionEntitlement(purchase); - - // ────────────────────────────────────────────────────────────────────── - // STEP 5: FINISH TRANSACTION - // ────────────────────────────────────────────────────────────────────── - // CRITICAL: Always finish/acknowledge transactions! - // - iOS: finishTransaction removes from StoreKit queue - // - Android: Acknowledges purchase (required within 3 days) - // - Subscriptions are NOT consumable (isConsumable: false) - const isConsumable = false; - - if (!connectedRef.current) { - console.log( - '[SubscriptionFlow] Skipping finishTransaction - not connected yet', - ); - const started = Date.now(); - const tryFinish = () => { - if (connectedRef.current) { - finishTransaction({ - purchase, - isConsumable, - }).catch((err) => { - console.warn( - '[SubscriptionFlow] Delayed finishTransaction failed:', - err, - ); - }); - return; - } - if (Date.now() - started < 30000) { - setTimeout(tryFinish, 500); - } - }; - setTimeout(tryFinish, 500); - } else { - await finishTransaction({ - purchase, - isConsumable, - }); - } - - // ────────────────────────────────────────────────────────────────────── - // STEP 6: REFRESH SUBSCRIPTION STATUS - // ────────────────────────────────────────────────────────────────────── - // After successful purchase, refresh active subscriptions to update UI. - // This ensures the user sees their new subscription immediately. - try { - await getActiveSubscriptions(SUBSCRIPTION_PRODUCT_IDS); - } catch (e) { - console.warn('Failed to refresh subscriptions:', e); - } - - Alert.alert('Success', 'Purchase completed successfully!'); - }, - - // ──────────────────────────────────────────────────────────────────────── - // Purchase Error Handler - // ──────────────────────────────────────────────────────────────────────── - onPurchaseError: (error: PurchaseError) => { - console.error('Subscription failed:', error); - setIsProcessing(false); - const dt = Date.now() - lastSuccessAtRef.current; - if (error?.code === ErrorCode.ServiceError && dt >= 0 && dt < 1500) { - return; - } - - setPurchaseResult(`❌ Subscription failed: ${error.message}`); - Alert.alert('Subscription Failed', error.message); - }, - }); - - useEffect(() => { - connectedRef.current = connected; - }, [connected]); - - // ────────────────────────────────────────────────────────────────────────── - // ON APP LAUNCH: Fetch available subscription products - // ────────────────────────────────────────────────────────────────────────── - // When the store connection is established, fetch subscription products. - // This populates the subscriptions array for display. - useEffect(() => { - if (connected) { - if (!fetchedProductsOnceRef.current) { - fetchProducts({ - skus: SUBSCRIPTION_PRODUCT_IDS, - type: 'subs', - }); - fetchedProductsOnceRef.current = true; - } - } - }, [connected, fetchProducts]); - - // 🔍 LOG: Check discount and promotional offer data - useEffect(() => { - if (subscriptions.length > 0) { - console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log('🔍 [SubscriptionFlow] DISCOUNT DATA CHECK:'); - console.log(` Total subscriptions: ${subscriptions.length}`); - - subscriptions.forEach((sub) => { - console.log(`\n 📦 Subscription: ${sub.id}`); - console.log(` • Title: ${sub.title}`); - const price = 'localizedPrice' in sub ? sub.localizedPrice : sub.price; - console.log(` • Price: ${price}`); - - // iOS specific fields - if (Platform.OS === 'ios' && 'introductoryPricePaymentModeIOS' in sub) { - console.log( - ` • introductoryPricePaymentModeIOS: ${ - sub.introductoryPricePaymentModeIOS || 'null' - }`, - ); - console.log( - ` • introductoryPriceIOS: ${ - sub.introductoryPriceIOS || 'null' - }`, - ); - - // Log discountsIOS (already parsed as DiscountIOS[]) - if (sub.discountsIOS && sub.discountsIOS.length > 0) { - console.log( - ` • discountsIOS: ${sub.discountsIOS.length} discount(s)`, - ); - sub.discountsIOS.forEach((discount, idx) => { - console.log( - ` [${idx}] type: ${discount.type}, paymentMode: ${discount.paymentMode}, price: ${discount.price}`, - ); - }); - } else { - console.log(' • discountsIOS: empty or null ⚠️'); - } - } - - // Cross-platform subscription offers - if ('subscriptionOffers' in sub && sub.subscriptionOffers) { - console.log( - ` • subscriptionOffers: ${sub.subscriptionOffers.length || 0} offer(s)`, - ); - } - - // Cross-platform discount offers - if ('discountOffers' in sub && sub.discountOffers) { - console.log( - ` • discountOffers: ${sub.discountOffers.length || 0} offer(s)`, - ); - } - }); - - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); - } - }, [subscriptions]); - - // ────────────────────────────────────────────────────────────────────────── - // CHECKING SUBSCRIPTION STATUS (Step 3 in lifecycle) - // ────────────────────────────────────────────────────────────────────────── - // Periodically verify subscription status to detect: - // - Cancellations (willAutoRenew = false) - // - Billing issues (isInBillingRetry = true) - // - Pending upgrades/downgrades (pendingUpgradeProductId) - // - Grace period status (gracePeriodExpirationDate) - // - // iOS: Rich data available via renewalInfoIOS - // Android: Basic data available; detailed info requires server-side API - const handleRefreshStatus = useCallback(async () => { - if (!connected || isCheckingStatus) return; - - setIsCheckingStatus(true); - try { - // Refresh active subscriptions - this is the key API for status checking - // Returns ActiveSubscription[] with platform-specific renewal info - const activeSubs = await getActiveSubscriptions(); - console.log('\n===== Active Subscriptions Check ====='); - console.log('Total subscriptions:', activeSubs.length); - console.log('Full data:', JSON.stringify(activeSubs, null, 2)); - - // For iOS, check if there's a pending change in renewalInfo - if (Platform.OS === 'ios') { - const premiumSubs = activeSubs.filter( - (sub) => - sub.productId === 'dev.hyo.martie.premium' || - sub.productId === 'dev.hyo.martie.premium_year', - ); - - console.log('\n===== iOS Subscription Analysis ====='); - console.log('Premium subscriptions found:', premiumSubs.length); - - premiumSubs.forEach((sub, index) => { - console.log(`\n[Subscription ${index + 1}]`); - console.log(' productId:', sub.productId); - console.log( - ' transactionDate:', - sub.transactionDate - ? new Date(sub.transactionDate).toISOString() - : 'N/A', - ); - console.log(' transactionId:', sub.transactionId); - console.log(' expirationDateIOS:', sub.expirationDateIOS); - console.log(' environmentIOS:', sub.environmentIOS); - - // 🔍 Check if renewalInfoIOS exists and log all fields - if (sub.renewalInfoIOS) { - console.log(' ✅ renewalInfoIOS EXISTS:'); - console.log(' willAutoRenew:', sub.renewalInfoIOS.willAutoRenew); - console.log( - ' pendingUpgradeProductId:', - sub.renewalInfoIOS.pendingUpgradeProductId, - ); - console.log( - ' autoRenewPreference:', - sub.renewalInfoIOS.autoRenewPreference, - ); - console.log(' renewalDate:', sub.renewalInfoIOS.renewalDate); - console.log( - ' expirationReason:', - sub.renewalInfoIOS.expirationReason, - ); - console.log( - ' isInBillingRetry:', - sub.renewalInfoIOS.isInBillingRetry, - ); - console.log( - ' gracePeriodExpirationDate:', - sub.renewalInfoIOS.gracePeriodExpirationDate, - ); - console.log( - ' priceIncreaseStatus:', - sub.renewalInfoIOS.priceIncreaseStatus, - ); - // 🆕 NEW FIELDS - Check if they're coming through correctly - console.log( - ' 🆕 renewalOfferType:', - sub.renewalInfoIOS.renewalOfferType, - ); - console.log( - ' 🆕 renewalOfferId:', - sub.renewalInfoIOS.renewalOfferId, - ); - console.log( - ' 🆕 jsonRepresentation:', - sub.renewalInfoIOS.jsonRepresentation - ? `<${sub.renewalInfoIOS.jsonRepresentation.substring( - 0, - 50, - )}...>` - : 'null/undefined', - ); - console.log( - ' Full renewalInfoIOS:', - JSON.stringify(sub.renewalInfoIOS, null, 2), - ); - } else { - console.log(' ❌ renewalInfoIOS is NULL/UNDEFINED'); - } - }); - console.log('===================================\n'); - } - } catch (error) { - console.error('Error checking subscription status:', error); - } finally { - setIsCheckingStatus(false); - } - }, [connected, getActiveSubscriptions, isCheckingStatus]); - - useEffect(() => { - if (connected && !statusAutoCheckedRef.current) { - const timer = setTimeout(() => { - statusAutoCheckedRef.current = true; - void handleRefreshStatus(); - }, 500); - - return () => clearTimeout(timer); - } - return undefined; - }, [connected, handleRefreshStatus]); - - // ────────────────────────────────────────────────────────────────────────── - // REQUEST SUBSCRIPTION PURCHASE - // ────────────────────────────────────────────────────────────────────────── - // Initiates a subscription purchase. Platform-specific handling: - // - // iOS: Uses sku and optional appAccountToken for user tracking - // StoreKit handles offer eligibility automatically - // - // Android: Requires subscriptionOffers with offerToken - // Each offer represents a base plan (monthly/yearly) or promotional offer - // The offerToken is obtained from subscriptionOffers (cross-platform type) - const handleSubscription = useCallback( - (itemId: string) => { - setIsProcessing(true); - setPurchaseResult('Processing subscription...'); - - const subscription = subscriptions.find((sub) => sub.id === itemId); - - void requestPurchase({ - request: { - ios: { - sku: itemId, - appAccountToken: 'user-123', - }, - android: { - skus: [itemId], - subscriptionOffers: - subscription && - 'subscriptionOffers' in subscription && - (subscription as ProductSubscriptionAndroid).subscriptionOffers - ? ( - subscription as ProductSubscriptionAndroid - ).subscriptionOffers.map((offer) => ({ - sku: itemId, - offerToken: offer.offerTokenAndroid ?? '', - })) - : [], - }, - }, - type: 'subs', - }).catch((err: PurchaseError) => { - console.warn('requestPurchase failed:', err); - setIsProcessing(false); - setPurchaseResult(`❌ Subscription failed: ${err.message}`); - Alert.alert('Subscription Failed', err.message); - }); - }, - [subscriptions], - ); - - // ────────────────────────────────────────────────────────────────────────── - // RESTORING PURCHASES - // ────────────────────────────────────────────────────────────────────────── - // Retry loading subscriptions (useful when products fail to load initially) - const handleRetryLoadSubscriptions = useCallback(() => { - fetchProducts({ - skus: SUBSCRIPTION_PRODUCT_IDS, - type: 'subs', - }); - }, [fetchProducts]); - - // ────────────────────────────────────────────────────────────────────────── - // MANAGE SUBSCRIPTIONS (Deep link to platform settings) - // ────────────────────────────────────────────────────────────────────────── - // Opens the platform's subscription management screen where users can: - // - Cancel subscriptions - // - Change subscription plans (upgrade/downgrade) - // - Update payment methods - // - View subscription history - const handleManageSubscriptions = useCallback(async () => { - try { - await deepLinkToSubscriptions(); - } catch (error) { - console.warn('Failed to open subscription management:', error); - Alert.alert( - 'Cannot Open', - 'Unable to open the subscription management screen on this device.', - ); - } - }, []); - - return ( - - ); -} - -export default SubscriptionFlowContainer; - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f7f9fc', - }, - header: { - padding: 24, - paddingTop: 48, - backgroundColor: '#1f3c88', - borderBottomLeftRadius: 24, - borderBottomRightRadius: 24, - }, - title: { - fontSize: 26, - fontWeight: 'bold', - color: 'white', - marginBottom: 8, - }, - subtitle: { - fontSize: 14, - color: 'rgba(255,255,255,0.85)', - lineHeight: 20, - }, - statusContainer: { - marginTop: 16, - padding: 12, - borderRadius: 12, - backgroundColor: 'rgba(255,255,255,0.15)', - }, - statusText: { - color: 'white', - fontSize: 13, - marginBottom: 4, - }, - verificationContainer: { - marginTop: 16, - padding: 12, - borderRadius: 12, - backgroundColor: 'rgba(255,255,255,0.15)', - }, - verificationLabel: { - color: 'white', - fontSize: 13, - marginBottom: 8, - fontWeight: '500', - }, - verificationButton: { - backgroundColor: 'rgba(255,255,255,0.2)', - borderRadius: 8, - padding: 12, - borderWidth: 1, - borderColor: 'rgba(255,255,255,0.3)', - }, - verificationButtonText: { - fontSize: 15, - fontWeight: '600', - color: 'white', - marginBottom: 4, - }, - verificationButtonHint: { - fontSize: 11, - color: 'rgba(255,255,255,0.7)', - fontStyle: 'italic', - }, - section: { - paddingHorizontal: 20, - paddingTop: 24, - }, - sectionHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - sectionTitle: { - fontSize: 20, - fontWeight: '700', - color: '#1a1f36', - }, - sectionSubtitle: { - fontSize: 13, - color: '#5f6470', - marginTop: 6, - }, - sectionDivider: { - height: 1, - backgroundColor: '#e1e7ef', - marginVertical: 20, - }, - subscriptionCard: { - backgroundColor: 'white', - borderRadius: 16, - padding: 18, - marginTop: 16, - shadowColor: '#000', - shadowOffset: {width: 0, height: 4}, - shadowOpacity: 0.05, - shadowRadius: 12, - elevation: 3, - }, - subscriptionHeader: { - flexDirection: 'row', - alignItems: 'flex-start', - gap: 12, - }, - subscriptionTitle: { - fontSize: 18, - fontWeight: '700', - color: '#1a1f36', - }, - subscriptionDescription: { - fontSize: 13, - color: '#5f6470', - marginTop: 4, - }, - subscriptionMeta: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginTop: 16, - }, - subscriptionPrice: { - fontSize: 20, - fontWeight: '700', - color: '#1a1f36', - }, - subscriptionPeriod: { - fontSize: 13, - color: '#5f6470', - }, - badgeIntroOffer: { - alignSelf: 'flex-start', - marginTop: 12, - paddingHorizontal: 10, - paddingVertical: 4, - borderRadius: 999, - backgroundColor: '#ff8c42', - }, - badgeIntroOfferText: { - color: 'white', - fontWeight: '600', - fontSize: 12, - textTransform: 'uppercase', - letterSpacing: 0.5, - }, - subscribeButton: { - marginTop: 16, - borderRadius: 12, - paddingVertical: 12, - alignItems: 'center', - backgroundColor: '#1f3c88', - }, - subscribeButtonText: { - color: 'white', - fontWeight: '700', - fontSize: 15, - }, - subscribeButtonOwned: { - backgroundColor: 'rgba(31,60,136,0.1)', - }, - subscribeButtonOwnedText: { - color: '#1f3c88', - }, - infoButton: { - paddingHorizontal: 10, - paddingVertical: 6, - borderRadius: 999, - backgroundColor: '#f1f4ff', - }, - infoButtonText: { - fontSize: 16, - }, - emptyState: { - backgroundColor: 'white', - borderRadius: 16, - padding: 24, - alignItems: 'center', - marginTop: 20, - }, - emptyStateText: { - fontSize: 14, - color: '#5f6470', - textAlign: 'center', - }, - retryButton: { - marginTop: 16, - paddingHorizontal: 24, - paddingVertical: 10, - borderRadius: 999, - backgroundColor: '#1f3c88', - }, - retryButtonText: { - color: 'white', - fontWeight: '600', - }, - statusSection: { - paddingTop: 32, - }, - statusCard: { - backgroundColor: 'white', - borderRadius: 16, - padding: 20, - marginTop: 16, - gap: 16, - }, - statusRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - statusLabel: { - fontSize: 13, - color: '#5f6470', - fontWeight: '600', - }, - statusValue: { - fontSize: 14, - fontWeight: '700', - color: '#1a1f36', - }, - activeStatus: { - color: '#1f8a70', - }, - cancelledStatus: { - color: '#d7263d', - }, - subscriptionStatusItem: { - padding: 16, - borderWidth: 1, - borderColor: '#e1e7ef', - borderRadius: 12, - }, - refreshButton: { - marginTop: 16, - borderRadius: 12, - paddingVertical: 12, - alignItems: 'center', - borderWidth: 1, - borderColor: '#1f3c88', - backgroundColor: 'white', - }, - refreshButtonText: { - color: '#1f3c88', - fontWeight: '600', - fontSize: 14, - }, - manageButton: { - paddingHorizontal: 16, - paddingVertical: 6, - borderRadius: 999, - backgroundColor: 'rgba(31,60,136,0.1)', - }, - manageButtonText: { - color: '#1f3c88', - fontWeight: '600', - }, - resultCard: { - marginTop: 16, - backgroundColor: '#e8f5e9', - borderRadius: 12, - padding: 16, - borderLeftWidth: 4, - borderLeftColor: '#1f8a70', - }, - resultText: { - fontSize: 13, - color: '#1a1f36', - fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace', - }, - latestPurchaseContainer: { - marginTop: 16, - gap: 12, - }, - latestPurchaseTitle: { - fontSize: 15, - fontWeight: '700', - color: '#1a1f36', - }, - modalOverlay: { - flex: 1, - backgroundColor: 'rgba(0,0,0,0.4)', - justifyContent: 'center', - alignItems: 'center', - }, - modalContainer: { - width: '90%', - maxHeight: '80%', - backgroundColor: 'white', - borderRadius: 16, - overflow: 'hidden', - }, - modalHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - padding: 18, - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: '#e1e7ef', - }, - modalTitle: { - fontSize: 18, - fontWeight: '700', - color: '#1a1f36', - }, - closeButton: { - padding: 6, - }, - closeButtonText: { - fontSize: 22, - color: '#5f6470', - }, - modalContent: { - padding: 18, - }, - modalLabel: { - fontSize: 12, - color: '#5f6470', - }, - modalValue: { - fontSize: 14, - color: '#1a1f36', - }, - jsonContainer: { - maxHeight: 320, - borderRadius: 12, - backgroundColor: '#f7f9fc', - padding: 16, - }, - jsonText: { - fontSize: 12, - color: '#1a1f36', - fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace', - }, - buttonContainer: { - flexDirection: 'row', - gap: 12, - marginTop: 18, - }, - actionButton: { - flex: 1, - paddingVertical: 12, - borderRadius: 12, - alignItems: 'center', - }, - copyButton: { - backgroundColor: '#1f3c88', - }, - consoleButton: { - backgroundColor: '#1f8a70', - }, - actionButtonText: { - color: 'white', - fontWeight: '700', - fontSize: 14, - }, - purchaseDetailsContainer: { - gap: 12, - }, - purchaseDetailRow: { - flexDirection: 'column', - gap: 6, - paddingVertical: 8, - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: '#e1e7ef', - }, - infoSection: { - margin: 24, - padding: 20, - borderRadius: 16, - backgroundColor: '#eef2ff', - }, - infoTitle: { - fontSize: 16, - fontWeight: '700', - color: '#1a1f36', - marginBottom: 12, - }, - infoText: { - fontSize: 13, - color: '#1a1f36', - marginBottom: 6, - }, - offerLabel: { - fontWeight: '600', - color: '#1f3c88', - }, - transactionId: { - fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace', - fontSize: 12, - color: '#5f6470', - }, - renewalInfoSection: { - marginTop: 12, - paddingTop: 12, - borderTopWidth: 1, - borderTopColor: '#e1e7ef', - }, - renewalInfoTitle: { - fontSize: 14, - fontWeight: '700', - color: '#1f3c88', - marginBottom: 8, - }, - upgradeText: { - color: '#3b82f6', - fontWeight: '600', - }, - cancelledText: { - color: '#f59e0b', - fontWeight: '600', - }, - billingRetryText: { - color: '#8b5cf6', - fontWeight: '600', - }, - planChangeSection: { - marginTop: 16, - marginBottom: 8, - }, - planChangeOptions: { - gap: 8, - }, - changePlanButton: { - paddingVertical: 14, - paddingHorizontal: 16, - borderRadius: 12, - alignItems: 'center', - justifyContent: 'center', - marginBottom: 8, - }, - changePlanButtonText: { - color: 'white', - fontWeight: '600', - fontSize: 15, - }, - changePlanButtonSubtext: { - color: 'rgba(255, 255, 255, 0.8)', - fontSize: 12, - marginTop: 4, - }, - upgradeButton: { - backgroundColor: '#4CAF50', - }, - downgradeButton: { - backgroundColor: '#FF9800', - }, - switchButton: { - backgroundColor: '#2196F3', - }, - selectButton: { - backgroundColor: '#9C27B0', - }, - warningText: { - fontSize: 13, - color: '#FF9800', - textAlign: 'center', - marginBottom: 12, - lineHeight: 18, - }, - questionText: { - fontSize: 14, - fontWeight: '600', - textAlign: 'center', - marginBottom: 12, - color: '#1a1f36', - }, - // Upgrade Detection Styles - upgradeDetectionSection: { - paddingTop: 20, - }, - upgradeDetectionCard: { - backgroundColor: '#fff3e0', - borderRadius: 16, - padding: 20, - marginTop: 16, - borderWidth: 2, - borderColor: '#ff9800', - }, - upgradeDetectionTitle: { - fontSize: 18, - fontWeight: '700', - color: '#e65100', - marginBottom: 8, - }, - upgradeDetectionSubtitle: { - fontSize: 14, - color: '#5f6470', - marginBottom: 16, - }, - upgradeFlowContainer: { - marginTop: 12, - gap: 12, - }, - upgradeProductBox: { - backgroundColor: 'white', - borderRadius: 12, - padding: 12, - borderWidth: 1, - borderColor: '#e1e7ef', - }, - upgradeProductLabel: { - fontSize: 12, - color: '#5f6470', - fontWeight: '600', - marginBottom: 4, - textTransform: 'uppercase', - }, - upgradeProductTitle: { - fontSize: 16, - fontWeight: '700', - color: '#1a1f36', - }, - upgradingProduct: { - color: '#ff9800', - }, - upgradeArrow: { - fontSize: 24, - color: '#ff9800', - textAlign: 'center', - fontWeight: '700', - }, - upgradeRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingVertical: 8, - }, - upgradeLabel: { - fontSize: 13, - color: '#5f6470', - fontWeight: '600', - }, - upgradeValue: { - fontSize: 14, - fontWeight: '700', - color: '#1a1f36', - }, - viewRenewalInfoButton: { - marginTop: 12, - borderRadius: 12, - paddingVertical: 10, - alignItems: 'center', - borderWidth: 1, - borderColor: '#ff9800', - backgroundColor: 'white', - }, - viewRenewalInfoButtonText: { - color: '#ff9800', - fontWeight: '600', - fontSize: 14, - }, - // Cancellation Detection Styles - cancellationDetectionSection: { - paddingTop: 20, - }, - cancellationDetectionCard: { - backgroundColor: '#fff9e6', - borderRadius: 16, - padding: 20, - marginTop: 16, - borderWidth: 2, - borderColor: '#ffc107', - }, - cancellationDetectionTitle: { - fontSize: 18, - fontWeight: '700', - color: '#f57c00', - marginBottom: 8, - }, - cancellationDetectionSubtitle: { - fontSize: 14, - color: '#5f6470', - marginBottom: 16, - }, - cancellationInfoContainer: { - marginTop: 12, - gap: 12, - }, - cancellationProductBox: { - backgroundColor: 'white', - borderRadius: 12, - padding: 12, - borderWidth: 1, - borderColor: '#e1e7ef', - }, - cancellationProductLabel: { - fontSize: 12, - color: '#5f6470', - fontWeight: '600', - marginBottom: 4, - textTransform: 'uppercase', - }, - cancellationProductTitle: { - fontSize: 16, - fontWeight: '700', - color: '#1a1f36', - marginBottom: 4, - }, - cancellationExpiryDate: { - fontSize: 14, - color: '#f57c00', - fontWeight: '600', - }, - // Offer Details Styles (Modal) - detailLabel: { - fontSize: 12, - color: '#5f6470', - fontWeight: '600', - marginTop: 8, - }, - detailValue: { - fontSize: 14, - color: '#1a1f36', - marginTop: 2, - marginBottom: 4, - }, - offersSection: { - marginTop: 20, - paddingTop: 15, - borderTopWidth: 1, - borderTopColor: '#e1e7ef', - }, - offersSectionTitle: { - fontSize: 16, - fontWeight: '700', - color: '#1a1f36', - marginBottom: 12, - }, - offerCard: { - backgroundColor: '#f1f4ff', - borderRadius: 12, - padding: 14, - marginBottom: 12, - borderLeftWidth: 4, - borderLeftColor: '#1f3c88', - }, - offerTitle: { - fontSize: 14, - fontWeight: '700', - color: '#1f3c88', - marginBottom: 10, - }, - offerDetailLabel: { - fontSize: 11, - color: '#5f6470', - marginTop: 8, - fontWeight: '600', - }, - offerValue: { - fontSize: 13, - color: '#1a1f36', - marginTop: 2, - }, - offerValueDiscount: { - fontSize: 13, - color: '#E53935', - marginTop: 2, - fontWeight: '700', - }, - offerTokenText: { - fontSize: 10, - color: '#999', - fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace', - }, - pricingPhase: { - backgroundColor: '#e3f2fd', - borderRadius: 6, - padding: 8, - marginTop: 6, - marginBottom: 2, - }, - phaseText: { - fontSize: 12, - color: '#1565C0', - fontWeight: '600', - }, - phaseDetail: { - fontSize: 10, - color: '#5f6470', - marginTop: 2, - }, - rawJsonSection: { - marginTop: 20, - paddingTop: 15, - borderTopWidth: 1, - borderTopColor: '#e1e7ef', - }, - rawJsonTitle: { - fontSize: 14, - fontWeight: '700', - color: '#5f6470', - marginBottom: 8, - }, -}); diff --git a/libraries/react-native-iap/example-expo/app/test.tsx b/libraries/react-native-iap/example-expo/app/test.tsx deleted file mode 100644 index 951d5432..00000000 --- a/libraries/react-native-iap/example-expo/app/test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import {View, Text, StyleSheet} from 'react-native'; - -export default function TestScreen() { - return ( - - Test Screen - This is a test screen without react-native-iap imports - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - padding: 20, - justifyContent: 'center', - alignItems: 'center', - }, - title: { - fontSize: 24, - fontWeight: 'bold', - marginBottom: 20, - }, -}); diff --git a/libraries/react-native-iap/example-expo/app/webhook-stream.tsx b/libraries/react-native-iap/example-expo/app/webhook-stream.tsx deleted file mode 100644 index 3b80e41c..00000000 --- a/libraries/react-native-iap/example-expo/app/webhook-stream.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import React, {useCallback, useEffect, useRef, useState} from 'react'; -import { - ActivityIndicator, - FlatList, - StyleSheet, - Text, - TouchableOpacity, - View, -} from 'react-native'; -import Constants from 'expo-constants'; -import { - connectWebhookStream, - type WebhookEventPayload, - type WebhookListener, - type WebhookListenerError, -} from 'react-native-iap'; - -function base64EncodeUtf8(input: string): string { - return btoa(unescape(encodeURIComponent(input))); -} - -export default function WebhookStreamScreen() { - const apiKey: string = (( - Constants.expoConfig?.extra as {iapkitApiKey?: string} | undefined - )?.iapkitApiKey ?? - process.env.EXPO_PUBLIC_IAPKIT_API_KEY ?? - '') as string; - const baseUrl = - process.env.EXPO_PUBLIC_IAPKIT_BASE_URL ?? 'https://kit.openiap.dev'; - - const [events, setEvents] = useState([]); - const [status, setStatus] = useState< - 'idle' | 'connecting' | 'connected' | 'error' - >('idle'); - const [statusMessage, setStatusMessage] = useState(null); - const [testing, setTesting] = useState(false); - const listenerRef = useRef(null); - - const startStream = useCallback(() => { - if (!apiKey) { - setStatus('error'); - setStatusMessage('Set EXPO_PUBLIC_IAPKIT_API_KEY to connect.'); - return; - } - listenerRef.current?.close(); - listenerRef.current = connectWebhookStream({ - apiKey, - baseUrl, - onEvent: (event) => { - setStatusMessage(null); - setEvents((prev) => [event, ...prev].slice(0, 50)); - }, - onError: (error: WebhookListenerError) => { - setStatus('error'); - setStatusMessage(`${error.code}: ${error.message}`); - }, - }); - setStatus('connected'); - setStatusMessage(null); - }, [apiKey, baseUrl]); - - const stopStream = useCallback(() => { - listenerRef.current?.close(); - listenerRef.current = null; - setStatus('idle'); - setStatusMessage(null); - }, []); - - useEffect(() => { - return () => { - listenerRef.current?.close(); - listenerRef.current = null; - }; - }, []); - - const triggerTestNotification = useCallback(async () => { - if (!apiKey) { - setStatusMessage('Cannot trigger test: API key missing.'); - return; - } - setTesting(true); - try { - const dataJson = JSON.stringify({ - version: '1.0', - packageName: 'com.example.app', - eventTimeMillis: String(Date.now()), - testNotification: {version: '1.0'}, - }); - const response = await fetch( - `${baseUrl.replace(/\/$/, '')}/v1/webhooks/${encodeURIComponent( - apiKey, - )}`, - { - method: 'POST', - headers: {'content-type': 'application/json'}, - body: JSON.stringify({ - message: { - data: base64EncodeUtf8(dataJson), - messageId: `rn-expo-test-${Date.now()}`, - publishTime: new Date().toISOString(), - }, - subscription: 'projects/example/subscriptions/iapkit-rtdn', - }), - }, - ); - setStatusMessage( - response.ok - ? 'Test notification accepted.' - : `Test POST returned ${response.status}: ${await response.text()}`, - ); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setStatusMessage(`Test POST failed: ${message}`); - } finally { - setTesting(false); - } - }, [apiKey, baseUrl]); - - return ( - - - Webhook Stream - SSE /v1/webhooks/stream/apiKey - - api key: {apiKey ? `${apiKey.slice(0, 8)}...` : 'MISSING'} - - - - {status === 'idle' || status === 'error' ? ( - - Connect - - ) : ( - - Disconnect - - )} - - {testing ? ( - - ) : ( - Trigger test notification - )} - - - - Status: {status} - {statusMessage ? ( - {statusMessage} - ) : null} - - item.id} - contentContainerStyle={styles.list} - ListEmptyComponent={ - No events yet. Connect to start. - } - renderItem={({item}) => ( - - {item.type} - - source: {item.source ?? '-'} / platform: {item.platform ?? '-'} - - - productId: {item.productId ?? '-'} - {'\n'}receivedAt:{' '} - {item.receivedAt - ? new Date(item.receivedAt).toLocaleString() - : '-'} - - - )} - /> - - ); -} - -const styles = StyleSheet.create({ - container: {flex: 1, backgroundColor: '#fff'}, - header: {padding: 20, borderBottomWidth: 1, borderBottomColor: '#e5e7eb'}, - title: {fontSize: 24, fontWeight: '700', color: '#111827'}, - subtitle: {marginTop: 4, color: '#4b5563'}, - meta: {marginTop: 8, color: '#6b7280', fontSize: 12}, - controls: {flexDirection: 'row', gap: 8, padding: 20}, - primaryButton: { - flex: 1, - backgroundColor: '#007AFF', - borderRadius: 10, - padding: 12, - alignItems: 'center', - }, - secondaryButton: { - flex: 1, - backgroundColor: '#FF3B30', - borderRadius: 10, - padding: 12, - alignItems: 'center', - }, - testButton: { - flex: 1, - backgroundColor: '#34C759', - borderRadius: 10, - padding: 12, - alignItems: 'center', - }, - buttonText: {color: '#fff', fontWeight: '600', textAlign: 'center'}, - status: {marginHorizontal: 20, padding: 12, borderRadius: 8}, - idle: {backgroundColor: '#f3f4f6'}, - connecting: {backgroundColor: '#fef3c7'}, - connected: {backgroundColor: '#dcfce7'}, - error: {backgroundColor: '#fee2e2'}, - statusText: {fontWeight: '600', color: '#111827'}, - statusMessage: {marginTop: 4, color: '#374151', fontSize: 12}, - list: {padding: 20, gap: 8}, - empty: {textAlign: 'center', color: '#6b7280', marginTop: 24}, - eventCard: { - padding: 12, - borderRadius: 10, - borderWidth: 1, - borderColor: '#e5e7eb', - backgroundColor: '#f9fafb', - }, - eventType: {fontWeight: '700', color: '#111827'}, - eventMeta: {marginTop: 4, fontSize: 12, color: '#4b5563'}, -}); diff --git a/libraries/react-native-iap/example-expo/assets/fonts/SpaceMono-Regular.ttf b/libraries/react-native-iap/example-expo/assets/fonts/SpaceMono-Regular.ttf deleted file mode 100755 index 28d7ff71..00000000 Binary files a/libraries/react-native-iap/example-expo/assets/fonts/SpaceMono-Regular.ttf and /dev/null differ diff --git a/libraries/react-native-iap/example-expo/assets/images/adaptive-icon.png b/libraries/react-native-iap/example-expo/assets/images/adaptive-icon.png deleted file mode 100644 index 861d8f5e..00000000 Binary files a/libraries/react-native-iap/example-expo/assets/images/adaptive-icon.png and /dev/null differ diff --git a/libraries/react-native-iap/example-expo/assets/images/favicon.png b/libraries/react-native-iap/example-expo/assets/images/favicon.png deleted file mode 100644 index 5c11223a..00000000 Binary files a/libraries/react-native-iap/example-expo/assets/images/favicon.png and /dev/null differ diff --git a/libraries/react-native-iap/example-expo/assets/images/icon.png b/libraries/react-native-iap/example-expo/assets/images/icon.png deleted file mode 100644 index 861d8f5e..00000000 Binary files a/libraries/react-native-iap/example-expo/assets/images/icon.png and /dev/null differ diff --git a/libraries/react-native-iap/example-expo/assets/images/partial-react-logo.png b/libraries/react-native-iap/example-expo/assets/images/partial-react-logo.png deleted file mode 100644 index 66fd9570..00000000 Binary files a/libraries/react-native-iap/example-expo/assets/images/partial-react-logo.png and /dev/null differ diff --git a/libraries/react-native-iap/example-expo/assets/images/react-logo.png b/libraries/react-native-iap/example-expo/assets/images/react-logo.png deleted file mode 100644 index 9d72a9ff..00000000 Binary files a/libraries/react-native-iap/example-expo/assets/images/react-logo.png and /dev/null differ diff --git a/libraries/react-native-iap/example-expo/assets/images/react-logo@2x.png b/libraries/react-native-iap/example-expo/assets/images/react-logo@2x.png deleted file mode 100644 index 2229b130..00000000 Binary files a/libraries/react-native-iap/example-expo/assets/images/react-logo@2x.png and /dev/null differ diff --git a/libraries/react-native-iap/example-expo/assets/images/react-logo@3x.png b/libraries/react-native-iap/example-expo/assets/images/react-logo@3x.png deleted file mode 100644 index a99b2032..00000000 Binary files a/libraries/react-native-iap/example-expo/assets/images/react-logo@3x.png and /dev/null differ diff --git a/libraries/react-native-iap/example-expo/assets/images/splash-icon.png b/libraries/react-native-iap/example-expo/assets/images/splash-icon.png deleted file mode 100644 index aa768605..00000000 Binary files a/libraries/react-native-iap/example-expo/assets/images/splash-icon.png and /dev/null differ diff --git a/libraries/react-native-iap/example-expo/bun.lock b/libraries/react-native-iap/example-expo/bun.lock deleted file mode 100644 index e51b81dc..00000000 --- a/libraries/react-native-iap/example-expo/bun.lock +++ /dev/null @@ -1,2085 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 0, - "workspaces": { - "": { - "name": "iapexpoexample", - "dependencies": { - "@expo/vector-icons": "^15.0.2", - "@preact/signals-react": "^3.3.0", - "@react-native-clipboard/clipboard": "^1.16.3", - "@react-navigation/bottom-tabs": "^7.3.10", - "@react-navigation/elements": "^2.3.8", - "@react-navigation/native": "^7.1.6", - "expo": "^54.0.12", - "expo-blur": "~15.0.7", - "expo-constants": "~18.0.9", - "expo-font": "~14.0.8", - "expo-haptics": "~15.0.7", - "expo-image": "~3.0.8", - "expo-linking": "~8.0.8", - "expo-router": "~6.0.10", - "expo-splash-screen": "~31.0.10", - "expo-status-bar": "~3.0.8", - "expo-symbols": "~1.0.7", - "expo-system-ui": "~6.0.7", - "expo-web-browser": "~15.0.8", - "react": "19.1.0", - "react-dom": "19.1.0", - "react-native": "0.81.4", - "react-native-gesture-handler": "~2.28.0", - "react-native-nitro-modules": "^0.35.0", - "react-native-reanimated": "~4.1.1", - "react-native-safe-area-context": "~5.6.0", - "react-native-screens": "~4.16.0", - "react-native-web": "^0.21.0", - "react-native-webview": "13.15.0", - }, - "devDependencies": { - "@babel/core": "^7.25.2", - "@types/react": "~19.1.10", - "eslint": "^9.25.0", - "eslint-config-expo": "~10.0.0", - "expo-build-properties": "~1.0.9", - "typescript": "~5.9.2", - }, - }, - }, - "packages": { - "@0no-co/graphql.web": ["@0no-co/graphql.web@1.2.0", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw=="], - - "@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=="], - - "@babel/compat-data": ["@babel/compat-data@7.28.0", "", {}, "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="], - - "@babel/core": ["@babel/core@7.28.3", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.3", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ=="], - - "@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="], - - "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], - - "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.3", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.3", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg=="], - - "@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ=="], - - "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], - - "@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=="], - - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], - - "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], - - "@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], - - "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@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-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=="], - - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - - "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="], - - "@babel/helpers": ["@babel/helpers@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.2" } }, "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw=="], - - "@babel/highlight": ["@babel/highlight@7.25.9", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw=="], - - "@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="], - - "@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.28.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-decorators": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg=="], - - "@babel/plugin-proposal-export-default-from": ["@babel/plugin-proposal-export-default-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw=="], - - "@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=="], - - "@babel/plugin-syntax-class-properties": ["@babel/plugin-syntax-class-properties@7.12.13", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA=="], - - "@babel/plugin-syntax-class-static-block": ["@babel/plugin-syntax-class-static-block@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw=="], - - "@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A=="], - - "@babel/plugin-syntax-dynamic-import": ["@babel/plugin-syntax-dynamic-import@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ=="], - - "@babel/plugin-syntax-export-default-from": ["@babel/plugin-syntax-export-default-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-eBC/3KSekshx19+N40MzjWqJd7KTEdOoLesAfa4IDFI8eRz5a47i5Oszus6zG/cwIXN63YhgLOMSSNJx49sENg=="], - - "@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-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=="], - - "@babel/plugin-syntax-json-strings": ["@babel/plugin-syntax-json-strings@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA=="], - - "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="], - - "@babel/plugin-syntax-logical-assignment-operators": ["@babel/plugin-syntax-logical-assignment-operators@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig=="], - - "@babel/plugin-syntax-nullish-coalescing-operator": ["@babel/plugin-syntax-nullish-coalescing-operator@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ=="], - - "@babel/plugin-syntax-numeric-separator": ["@babel/plugin-syntax-numeric-separator@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug=="], - - "@babel/plugin-syntax-object-rest-spread": ["@babel/plugin-syntax-object-rest-spread@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA=="], - - "@babel/plugin-syntax-optional-catch-binding": ["@babel/plugin-syntax-optional-catch-binding@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q=="], - - "@babel/plugin-syntax-optional-chaining": ["@babel/plugin-syntax-optional-chaining@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg=="], - - "@babel/plugin-syntax-private-property-in-object": ["@babel/plugin-syntax-private-property-in-object@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg=="], - - "@babel/plugin-syntax-top-level-await": ["@babel/plugin-syntax-top-level-await@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw=="], - - "@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-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.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=="], - - "@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q=="], - - "@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="], - - "@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.28.3", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg=="], - - "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.3", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg=="], - - "@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/template": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw=="], - - "@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-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.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-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.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw=="], - - "@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-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-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.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-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=="], - - "@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg=="], - - "@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA=="], - - "@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ=="], - - "@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA=="], - - "@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/types": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw=="], - - "@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.27.1", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q=="], - - "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], - - "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], - - "@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.28.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A=="], - - "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.28.3", "", { "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-Y6ab1kGqZ0u42Zv/4a7l0l72n9DKP/MKoKWaUSBylrhNZO2prYuqFOLbn5aW5SIFXwSH93yfjbgllL8lxuGKLg=="], - - "@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ=="], - - "@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@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-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q=="], - - "@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g=="], - - "@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-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-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/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/runtime": ["@babel/runtime@7.28.3", "", {}, "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA=="], - - "@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=="], - - "@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="], - - "@babel/traverse--for-generate-function-map": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="], - - "@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], - - "@egjs/hammerjs": ["@egjs/hammerjs@2.0.17", "", { "dependencies": { "@types/hammerjs": "^2.0.36" } }, "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A=="], - - "@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="], - - "@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], - - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], - - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], - - "@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="], - - "@eslint/config-helpers": ["@eslint/config-helpers@0.3.1", "", {}, "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA=="], - - "@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="], - - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], - - "@eslint/js": ["@eslint/js@9.34.0", "", {}, "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw=="], - - "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], - - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="], - - "@expo/cli": ["@expo/cli@54.0.10", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.8", "@expo/code-signing-certificates": "^0.0.5", "@expo/config": "~12.0.10", "@expo/config-plugins": "~54.0.2", "@expo/devcert": "^1.1.2", "@expo/env": "~2.0.7", "@expo/image-utils": "^0.8.7", "@expo/json-file": "^10.0.7", "@expo/mcp-tunnel": "~0.0.7", "@expo/metro": "~54.0.0", "@expo/metro-config": "~54.0.6", "@expo/osascript": "^2.3.7", "@expo/package-manager": "^1.9.8", "@expo/plist": "^0.4.7", "@expo/prebuild-config": "^54.0.4", "@expo/schema-utils": "^0.1.7", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.3.0", "@react-native/dev-middleware": "0.81.4", "@urql/core": "^5.0.6", "@urql/exchange-retry": "^1.3.0", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "env-editor": "^0.4.1", "expo-server": "^1.0.0", "freeport-async": "^2.0.0", "getenv": "^2.0.0", "glob": "^10.4.2", "lan-network": "^0.1.6", "minimatch": "^9.0.0", "node-forge": "^1.3.1", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^3.0.1", "pretty-bytes": "^5.6.0", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "qrcode-terminal": "0.11.0", "require-from-string": "^2.0.2", "requireg": "^0.2.2", "resolve": "^1.22.2", "resolve-from": "^5.0.0", "resolve.exports": "^2.0.3", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "tar": "^7.4.3", "terminal-link": "^2.1.1", "undici": "^6.18.2", "wrap-ansi": "^7.0.0", "ws": "^8.12.1" }, "peerDependencies": { "expo": "*", "expo-router": "*", "react-native": "*" }, "optionalPeers": ["expo-router", "react-native"], "bin": { "expo-internal": "build/bin/cli" } }, "sha512-iw9gAnN6+PKWWLIyYmiskY/wzZjuFMctunqGXuC8BGATWgtr/HpzjVqWbcL3KIX/GvEBCCh74Tkckrh+Ylxh5Q=="], - - "@expo/code-signing-certificates": ["@expo/code-signing-certificates@0.0.5", "", { "dependencies": { "node-forge": "^1.2.1", "nullthrows": "^1.1.1" } }, "sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw=="], - - "@expo/config": ["@expo/config@12.0.10", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "@expo/config-plugins": "~54.0.2", "@expo/config-types": "^54.0.8", "@expo/json-file": "^10.0.7", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^10.4.2", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4", "sucrase": "3.35.0" } }, "sha512-lJMof5Nqakq1DxGYlghYB/ogSBjmv4Fxn1ovyDmcjlRsQdFCXgu06gEUogkhPtc9wBt9WlTTfqENln5HHyLW6w=="], - - "@expo/config-plugins": ["@expo/config-plugins@54.0.2", "", { "dependencies": { "@expo/config-types": "^54.0.8", "@expo/json-file": "~10.0.7", "@expo/plist": "^0.4.7", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^2.0.0", "glob": "^10.4.2", "resolve-from": "^5.0.0", "semver": "^7.5.4", "slash": "^3.0.0", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-jD4qxFcURQUVsUFGMcbo63a/AnviK8WUGard+yrdQE3ZrB/aurn68SlApjirQQLEizhjI5Ar2ufqflOBlNpyPg=="], - - "@expo/config-types": ["@expo/config-types@54.0.8", "", {}, "sha512-lyIn/x/Yz0SgHL7IGWtgTLg6TJWC9vL7489++0hzCHZ4iGjVcfZmPTUfiragZ3HycFFj899qN0jlhl49IHa94A=="], - - "@expo/devcert": ["@expo/devcert@1.2.0", "", { "dependencies": { "@expo/sudo-prompt": "^9.3.1", "debug": "^3.1.0", "glob": "^10.4.2" } }, "sha512-Uilcv3xGELD5t/b0eM4cxBFEKQRIivB3v7i+VhWLV/gL98aw810unLKKJbGAxAIhY6Ipyz8ChWibFsKFXYwstA=="], - - "@expo/devtools": ["@expo/devtools@0.1.7", "", { "dependencies": { "chalk": "^4.1.2" }, "peerDependencies": { "react": "*", "react-native": "*" }, "optionalPeers": ["react", "react-native"] }, "sha512-dfIa9qMyXN+0RfU6SN4rKeXZyzKWsnz6xBSDccjL4IRiE+fQ0t84zg0yxgN4t/WK2JU5v6v4fby7W7Crv9gJvA=="], - - "@expo/env": ["@expo/env@2.0.7", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^2.0.0" } }, "sha512-BNETbLEohk3HQ2LxwwezpG8pq+h7Fs7/vAMP3eAtFT1BCpprLYoBBFZH7gW4aqGfqOcVP4Lc91j014verrYNGg=="], - - "@expo/fingerprint": ["@expo/fingerprint@0.15.1", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "getenv": "^2.0.0", "glob": "^10.4.2", "ignore": "^5.3.1", "minimatch": "^9.0.0", "p-limit": "^3.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-U1S9DwiapCHQjHdHDDyO/oXsl/1oEHSHZRRkWDDrHgXRUDiAVIySw9Unvvcr118Ee6/x4NmKSZY1X0VagrqmFg=="], - - "@expo/image-utils": ["@expo/image-utils@0.8.7", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "getenv": "^2.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "resolve-from": "^5.0.0", "resolve-global": "^1.0.0", "semver": "^7.6.0", "temp-dir": "~2.0.0", "unique-string": "~2.0.0" } }, "sha512-SXOww4Wq3RVXLyOaXiCCuQFguCDh8mmaHBv54h/R29wGl4jRY8GEyQEx8SypV/iHt1FbzsU/X3Qbcd9afm2W2w=="], - - "@expo/json-file": ["@expo/json-file@10.0.7", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "json5": "^2.2.3" } }, "sha512-z2OTC0XNO6riZu98EjdNHC05l51ySeTto6GP7oSQrCvQgG9ARBwD1YvMQaVZ9wU7p/4LzSf1O7tckL3B45fPpw=="], - - "@expo/mcp-tunnel": ["@expo/mcp-tunnel@0.0.8", "", { "dependencies": { "ws": "^8.18.3", "zod": "^3.25.76", "zod-to-json-schema": "^3.24.6" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.13.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-6261obzt6h9TQb6clET7Fw4Ig4AY2hfTNKI3gBt0gcTNxZipwMg8wER7ssDYieA9feD/FfPTuCPYFcR280aaWA=="], - - "@expo/metro": ["@expo/metro@54.0.0", "", { "dependencies": { "metro": "0.83.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-transform-plugins": "0.83.1", "metro-transform-worker": "0.83.1" } }, "sha512-x2HlliepLJVLSe0Fl/LuPT83Mn2EXpPlb1ngVtcawlz4IfbkYJo16/Zfsfrn1t9d8LpN5dD44Dc55Q1/fO05Nw=="], - - "@expo/metro-config": ["@expo/metro-config@54.0.6", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@expo/config": "~12.0.10", "@expo/env": "~2.0.7", "@expo/json-file": "~10.0.7", "@expo/metro": "~54.0.0", "@expo/spawn-async": "^1.7.2", "browserslist": "^4.25.0", "chalk": "^4.1.0", "debug": "^4.3.2", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^2.0.0", "glob": "^10.4.2", "hermes-parser": "^0.29.1", "jsc-safe-url": "^0.2.4", "lightningcss": "^1.30.1", "minimatch": "^9.0.0", "postcss": "~8.4.32", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*" }, "optionalPeers": ["expo"] }, "sha512-z3wufTr1skM03PI6Dr1ZsrvjAiGKf/w0VQvdZL+mEnKNqRA7Q4bhJDGk1+nzs+WWRWz4vS488uad9ERmSclBmg=="], - - "@expo/metro-runtime": ["@expo/metro-runtime@5.0.4", "", { "peerDependencies": { "react-native": "*" } }, "sha512-r694MeO+7Vi8IwOsDIDzH/Q5RPMt1kUDYbiTJwnO15nIqiDwlE8HU55UlRhffKZy6s5FmxQsZ8HA+T8DqUW8cQ=="], - - "@expo/osascript": ["@expo/osascript@2.3.7", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "exec-async": "^2.2.0" } }, "sha512-IClSOXxR0YUFxIriUJVqyYki7lLMIHrrzOaP01yxAL1G8pj2DWV5eW1y5jSzIcIfSCNhtGsshGd1tU/AYup5iQ=="], - - "@expo/package-manager": ["@expo/package-manager@1.9.8", "", { "dependencies": { "@expo/json-file": "^10.0.7", "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "resolve-workspace-root": "^2.0.0" } }, "sha512-4/I6OWquKXYnzo38pkISHCOCOXxfeEmu4uDoERq1Ei/9Ur/s9y3kLbAamEkitUkDC7gHk1INxRWEfFNzGbmOrA=="], - - "@expo/plist": ["@expo/plist@0.4.7", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.2.3", "xmlbuilder": "^15.1.1" } }, "sha512-dGxqHPvCZKeRKDU1sJZMmuyVtcASuSYh1LPFVaM1DuffqPL36n6FMEL0iUqq2Tx3xhWk8wCnWl34IKplUjJDdA=="], - - "@expo/prebuild-config": ["@expo/prebuild-config@54.0.4", "", { "dependencies": { "@expo/config": "~12.0.9", "@expo/config-plugins": "~54.0.2", "@expo/config-types": "^54.0.8", "@expo/image-utils": "^0.8.7", "@expo/json-file": "^10.0.7", "@react-native/normalize-colors": "0.81.4", "debug": "^4.3.1", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-X+oTbmclWf2kfWIEkjagOzPZNg2SkiWW+JoRX6CWxKpDTQKfsi/bf22Ymv5Zxe1Q/aGjOuFL5useStm3iNi+PA=="], - - "@expo/schema-utils": ["@expo/schema-utils@0.1.7", "", {}, "sha512-jWHoSuwRb5ZczjahrychMJ3GWZu54jK9ulNdh1d4OzAEq672K9E5yOlnlBsfIHWHGzUAT+0CL7Yt1INiXTz68g=="], - - "@expo/sdk-runtime-versions": ["@expo/sdk-runtime-versions@1.0.0", "", {}, "sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ=="], - - "@expo/spawn-async": ["@expo/spawn-async@1.7.2", "", { "dependencies": { "cross-spawn": "^7.0.3" } }, "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew=="], - - "@expo/sudo-prompt": ["@expo/sudo-prompt@9.3.2", "", {}, "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw=="], - - "@expo/vector-icons": ["@expo/vector-icons@15.0.2", "", { "peerDependencies": { "expo-font": ">=14.0.4", "react": "*", "react-native": "*" } }, "sha512-IiBjg7ZikueuHNf40wSGCf0zS73a3guJLdZzKnDUxsauB8VWPLMeWnRIupc+7cFhLUkqyvyo0jLNlcxG5xPOuQ=="], - - "@expo/ws-tunnel": ["@expo/ws-tunnel@1.0.6", "", {}, "sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q=="], - - "@expo/xcpretty": ["@expo/xcpretty@4.3.2", "", { "dependencies": { "@babel/code-frame": "7.10.4", "chalk": "^4.1.0", "find-up": "^5.0.0", "js-yaml": "^4.1.0" }, "bin": { "excpretty": "build/cli.js" } }, "sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw=="], - - "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], - - "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], - - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - - "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - - "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], - - "@isaacs/ttlcache": ["@isaacs/ttlcache@1.4.1", "", {}, "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA=="], - - "@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="], - - "@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="], - - "@jest/create-cache-key-function": ["@jest/create-cache-key-function@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3" } }, "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA=="], - - "@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], - - "@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="], - - "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "@jest/transform": ["@jest/transform@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", "write-file-atomic": "^4.0.2" } }, "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw=="], - - "@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="], - - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], - - "@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=="], - - "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], - - "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], - - "@preact/signals-core": ["@preact/signals-core@1.12.1", "", {}, "sha512-BwbTXpj+9QutoZLQvbttRg5x3l5468qaV2kufh+51yha1c53ep5dY4kTuZR35+3pAZxpfQerGJiQqg34ZNZ6uA=="], - - "@preact/signals-react": ["@preact/signals-react@3.3.0", "", { "dependencies": { "@preact/signals-core": "^1.12.0", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-Hxb7jQVuEA5y6EzlENcjpJLoxMf2rwUYU3KdJMHS+nYbA69+8elRbu6upiAOWtleXV4K7GZGQAD3KxB3Wk43KQ=="], - - "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], - - "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], - - "@radix-ui/react-compose-refs": ["@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-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], - - "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], - - "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], - - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], - - "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], - - "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], - - "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], - - "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], - - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], - - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], - - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "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-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="], - - "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], - - "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], - - "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], - - "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], - - "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], - - "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - - "@react-native-clipboard/clipboard": ["@react-native-clipboard/clipboard@1.16.3", "", { "peerDependencies": { "react": ">= 16.9.0", "react-native": ">= 0.61.5", "react-native-macos": ">= 0.61.0", "react-native-windows": ">= 0.61.0" }, "optionalPeers": ["react-native-macos", "react-native-windows"] }, "sha512-cMIcvoZKIrShzJHEaHbTAp458R9WOv0fB6UyC7Ek4Qk561Ow/DrzmmJmH/rAZg21Z6ixJ4YSdFDC14crqIBmCQ=="], - - "@react-native/assets-registry": ["@react-native/assets-registry@0.81.4", "", {}, "sha512-AMcDadefBIjD10BRqkWw+W/VdvXEomR6aEZ0fhQRAv7igrBzb4PTn4vHKYg+sUK0e3wa74kcMy2DLc/HtnGcMA=="], - - "@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.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/community-cli-plugin": ["@react-native/community-cli-plugin@0.81.4", "", { "dependencies": { "@react-native/dev-middleware": "0.81.4", "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-8mpnvfcLcnVh+t1ok6V9eozWo8Ut+TZhz8ylJ6gF9d6q9EGDQX6s8jenan5Yv/pzN4vQEKI4ib2pTf/FELw+SA=="], - - "@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.81.4", "", {}, "sha512-SU05w1wD0nKdQFcuNC9D6De0ITnINCi8MEnx9RsTD2e4wN83ukoC7FpXaPCYyP6+VjFt5tUKDPgP1O7iaNXCqg=="], - - "@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.81.4", "", {}, "sha512-T7fPcQvDDCSusZFVSg6H1oVDKb/NnVYLnsqkcHsAF2C2KGXyo3J7slH/tJAwNfj/7EOA2OgcWxfC1frgn9TQvw=="], - - "@react-native/js-polyfills": ["@react-native/js-polyfills@0.81.4", "", {}, "sha512-sr42FaypKXJHMVHhgSbu2f/ZJfrLzgaoQ+HdpRvKEiEh2mhFf6XzZwecyLBvWqf2pMPZa+CpPfNPiejXjKEy8w=="], - - "@react-native/normalize-colors": ["@react-native/normalize-colors@0.81.4", "", {}, "sha512-9nRRHO1H+tcFqjb9gAM105Urtgcanbta2tuqCVY0NATHeFPDEAB7gPyiLxCHKMi1NbhP6TH0kxgSWXKZl1cyRg=="], - - "@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.81.4", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.1.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-hBM+rMyL6Wm1Q4f/WpqGsaCojKSNUBqAXLABNGoWm1vabZ7cSnARMxBvA/2vo3hLcoR4v7zDK8tkKm9+O0LjVA=="], - - "@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.4.7", "", { "dependencies": { "@react-navigation/elements": "^2.6.4", "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": "^7.1.17", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-SQ4KuYV9yr3SV/thefpLWhAD0CU2CrBMG1l0w/QKl3GYuGWdN5OQmdQdmaPZGtsjjVOb+N9Qo7Tf6210P4TlpA=="], - - "@react-navigation/core": ["@react-navigation/core@7.12.4", "", { "dependencies": { "@react-navigation/routers": "^7.5.1", "escape-string-regexp": "^4.0.0", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-xLFho76FA7v500XID5z/8YfGTvjQPw7/fXsq4BIrVSqetNe/o/v+KAocEw4ots6kyv3XvSTyiWKh2g3pN6xZ9Q=="], - - "@react-navigation/elements": ["@react-navigation/elements@2.6.4", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.1.17", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-O3X9vWXOEhAO56zkQS7KaDzL8BvjlwZ0LGSteKpt1/k6w6HONG+2Wkblrb057iKmehTkEkQMzMLkXiuLmN5x9Q=="], - - "@react-navigation/native": ["@react-navigation/native@7.1.17", "", { "dependencies": { "@react-navigation/core": "^7.12.4", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*" } }, "sha512-uEcYWi1NV+2Qe1oELfp9b5hTYekqWATv2cuwcOAg5EvsIsUPtzFrKIasgUXLBRGb9P7yR5ifoJ+ug4u6jdqSTQ=="], - - "@react-navigation/native-stack": ["@react-navigation/native-stack@7.3.26", "", { "dependencies": { "@react-navigation/elements": "^2.6.4", "warn-once": "^0.1.1" }, "peerDependencies": { "@react-navigation/native": "^7.1.17", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-EjaBWzLZ76HJGOOcWCFf+h/M+Zg7M1RalYioDOb6ZdXHz7AwYNidruT3OUAQgSzg3gVLqvu5OYO0jFsNDPCZxQ=="], - - "@react-navigation/routers": ["@react-navigation/routers@7.5.1", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-pxipMW/iEBSUrjxz2cDD7fNwkqR4xoi0E/PcfTQGCcdJwLoaxzab5kSadBLj1MTJyT0YRrOXL9umHpXtp+Dv4w=="], - - "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], - - "@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], - - "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], - - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], - - "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], - - "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], - - "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], - - "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - - "@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="], - - "@types/hammerjs": ["@types/hammerjs@2.0.46", "", {}, "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw=="], - - "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], - - "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="], - - "@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="], - - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - - "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], - - "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], - - "@types/react": ["@types/react@19.1.17", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA=="], - - "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], - - "@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="], - - "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], - - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.41.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/type-utils": "8.41.0", "@typescript-eslint/utils": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.41.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw=="], - - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.41.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", "@typescript-eslint/typescript-estree": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg=="], - - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.41.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.41.0", "@typescript-eslint/types": "^8.41.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ=="], - - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0" } }, "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ=="], - - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.41.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw=="], - - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "@typescript-eslint/typescript-estree": "8.41.0", "@typescript-eslint/utils": "8.41.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ=="], - - "@typescript-eslint/types": ["@typescript-eslint/types@8.41.0", "", {}, "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag=="], - - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.41.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.41.0", "@typescript-eslint/tsconfig-utils": "8.41.0", "@typescript-eslint/types": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ=="], - - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.41.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", "@typescript-eslint/typescript-estree": "8.41.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A=="], - - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.41.0", "", { "dependencies": { "@typescript-eslint/types": "8.41.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg=="], - - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - - "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], - - "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="], - - "@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.11.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g=="], - - "@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.11.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ=="], - - "@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.11.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw=="], - - "@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw=="], - - "@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw=="], - - "@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ=="], - - "@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w=="], - - "@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.11.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA=="], - - "@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ=="], - - "@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew=="], - - "@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.11.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg=="], - - "@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w=="], - - "@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA=="], - - "@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.11.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ=="], - - "@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.11.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw=="], - - "@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.11.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ=="], - - "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], - - "@urql/core": ["@urql/core@5.2.0", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.13", "wonka": "^6.3.2" } }, "sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A=="], - - "@urql/exchange-retry": ["@urql/exchange-retry@1.3.2", "", { "dependencies": { "@urql/core": "^5.1.2", "wonka": "^6.3.2" } }, "sha512-TQMCz2pFJMfpNxmSfX1VSfTjwUIFx/mL+p1bnfM1xjjdla7Z+KnGMW/EhFbpckp3LyWAH4PgOsMwOMnIN+MBFg=="], - - "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="], - - "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], - - "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - - "anser": ["anser@1.4.10", "", {}, "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww=="], - - "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], - - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], - - "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], - - "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], - - "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], - - "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], - - "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], - - "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], - - "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], - - "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], - - "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], - - "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], - - "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], - - "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], - - "async-limiter": ["async-limiter@1.0.1", "", {}, "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="], - - "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - - "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=="], - - "babel-plugin-jest-hoist": ["babel-plugin-jest-hoist@29.6.3", "", { "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" } }, "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg=="], - - "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.14", "", { "dependencies": { "@babel/compat-data": "^7.27.7", "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg=="], - - "babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.13.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A=="], - - "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg=="], - - "babel-plugin-react-compiler": ["babel-plugin-react-compiler@19.1.0-rc.1-rc-af1b7da-20250421", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-E3kaokBhWDLf7ZD8fuYjYn0ZJHYZ+3EHtAWCdX2hl4lpu1z9S/Xr99sxhx2bTCVB41oIesz9FtM8f4INsrZaOw=="], - - "babel-plugin-react-native-web": ["babel-plugin-react-native-web@0.21.1", "", {}, "sha512-7XywfJ5QIRMwjOL+pwJt2w47Jmi5fFLvK7/So4fV4jIN6PcRbylCp9/l3cJY4VJbSz3lnWTeHDTD1LKIc1C09Q=="], - - "babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.29.1", "", { "dependencies": { "hermes-parser": "0.29.1" } }, "sha512-2WFYnoWGdmih1I1J5eIqxATOeycOqRwYxAQBu3cUu/rhwInwHUg7k60AFNbuGjSDL8tje5GDrAnxzRLcu2pYcA=="], - - "babel-plugin-transform-flow-enums": ["babel-plugin-transform-flow-enums@0.0.2", "", { "dependencies": { "@babel/plugin-syntax-flow": "^7.12.1" } }, "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ=="], - - "babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.2.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg=="], - - "babel-preset-expo": ["babel-preset-expo@54.0.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@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-runtime": "^7.24.7", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.81.4", "babel-plugin-react-compiler": "^19.1.0-rc.2", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.29.1", "babel-plugin-transform-flow-enums": "^0.0.2", "debug": "^4.3.4", "resolve-from": "^5.0.0" }, "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", "react-refresh": ">=0.14.0 <1.0.0" }, "optionalPeers": ["@babel/runtime", "expo"] }, "sha512-zC6g96Mbf1bofnCI8yI0VKAp8/ER/gpfTsWOpQvStbHU+E4jFZ294n3unW8Hf6nNP4NoeNq9Zc6Prp0vwhxbow=="], - - "babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="], - - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "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=="], - - "bplist-creator": ["bplist-creator@0.1.0", "", { "dependencies": { "stream-buffers": "2.2.x" } }, "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg=="], - - "bplist-parser": ["bplist-parser@0.3.2", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ=="], - - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "browserslist": ["browserslist@4.25.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg=="], - - "bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="], - - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - - "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "caller-callsite": ["caller-callsite@2.0.0", "", { "dependencies": { "callsites": "^2.0.0" } }, "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ=="], - - "caller-path": ["caller-path@2.0.0", "", { "dependencies": { "caller-callsite": "^2.0.0" } }, "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A=="], - - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - - "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], - - "caniuse-lite": ["caniuse-lite@1.0.30001739", "", {}, "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA=="], - - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], - - "chrome-launcher": ["chrome-launcher@0.15.2", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0" }, "bin": { "print-chrome-path": "bin/print-chrome-path.js" } }, "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ=="], - - "chromium-edge-launcher": ["chromium-edge-launcher@0.2.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0", "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg=="], - - "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - - "cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], - - "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - - "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], - - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], - - "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], - - "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - - "compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="], - - "compression": ["compression@1.8.1", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - - "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.45.1", "", { "dependencies": { "browserslist": "^4.25.3" } }, "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA=="], - - "cosmiconfig": ["cosmiconfig@5.2.1", "", { "dependencies": { "import-fresh": "^2.0.0", "is-directory": "^0.3.1", "js-yaml": "^3.13.1", "parse-json": "^4.0.0" } }, "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA=="], - - "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="], - - "css-in-js-utils": ["css-in-js-utils@3.1.0", "", { "dependencies": { "hyphenate-style-name": "^1.0.3" } }, "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A=="], - - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - - "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], - - "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], - - "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], - - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - - "decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="], - - "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], - - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - - "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - - "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], - - "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], - - "define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], - - "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], - - "detect-libc": ["detect-libc@2.1.1", "", {}, "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw=="], - - "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], - - "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], - - "dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="], - - "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "electron-to-chromium": ["electron-to-chromium@1.5.211", "", {}, "sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw=="], - - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "env-editor": ["env-editor@0.4.2", "", {}, "sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA=="], - - "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="], - - "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], - - "es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-iterator-helpers": ["es-iterator-helpers@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.6", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.4", "safe-array-concat": "^1.1.3" } }, "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - - "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], - - "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "eslint": ["eslint@9.34.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.34.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg=="], - - "eslint-config-expo": ["eslint-config-expo@10.0.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "^8.18.2", "@typescript-eslint/parser": "^8.18.2", "eslint-import-resolver-typescript": "^3.6.3", "eslint-plugin-expo": "^1.0.0", "eslint-plugin-import": "^2.30.0", "eslint-plugin-react": "^7.37.3", "eslint-plugin-react-hooks": "^5.1.0", "globals": "^16.0.0" }, "peerDependencies": { "eslint": ">=8.10" } }, "sha512-/XC/DvniUWTzU7Ypb/cLDhDD4DXqEio4lug1ObD/oQ9Hcx3OVOR8Mkp4u6U4iGoZSJyIQmIk3WVHe/P1NYUXKw=="], - - "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="], - - "eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.10.1", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.13", "unrs-resolver": "^1.6.2" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ=="], - - "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], - - "eslint-plugin-expo": ["eslint-plugin-expo@1.0.0", "", { "dependencies": { "@typescript-eslint/types": "^8.29.1", "@typescript-eslint/utils": "^8.29.1", "eslint": "^9.24.0" } }, "sha512-qLtunR+cNFtC+jwYCBia5c/PJurMjSLMOV78KrEOyQK02ohZapU4dCFFnS2hfrJuw0zxfsjVkjqg3QBqi933QA=="], - - "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], - - "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], - - "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], - - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], - - "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - - "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], - - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], - - "exec-async": ["exec-async@2.2.0", "", {}, "sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw=="], - - "expo": ["expo@54.0.12", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "54.0.10", "@expo/config": "~12.0.10", "@expo/config-plugins": "~54.0.2", "@expo/devtools": "0.1.7", "@expo/fingerprint": "0.15.1", "@expo/metro": "~54.0.0", "@expo/metro-config": "54.0.6", "@expo/vector-icons": "^15.0.2", "@ungap/structured-clone": "^1.3.0", "babel-preset-expo": "~54.0.3", "expo-asset": "~12.0.9", "expo-constants": "~18.0.9", "expo-file-system": "~19.0.16", "expo-font": "~14.0.8", "expo-keep-awake": "~15.0.7", "expo-modules-autolinking": "3.0.14", "expo-modules-core": "3.0.20", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli", "fingerprint": "bin/fingerprint", "expo-modules-autolinking": "bin/autolinking" } }, "sha512-BVvG1A9BlKAOBwczMi7XThOLzI3TUShkV/yRnAMGvQP5SQFDq7UojkZLLG285gg3OvkoqjMUE0tZvVXbvuI4tA=="], - - "expo-asset": ["expo-asset@12.0.9", "", { "dependencies": { "@expo/image-utils": "^0.8.7", "expo-constants": "~18.0.9" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-vrdRoyhGhBmd0nJcssTSk1Ypx3Mbn/eXaaBCQVkL0MJ8IOZpAObAjfD5CTy8+8RofcHEQdh3wwZVCs7crvfOeg=="], - - "expo-blur": ["expo-blur@15.0.7", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-SugQQbQd+zRPy8z2G5qDD4NqhcD7srBF7fN7O7yq6q7ZFK59VWvpDxtMoUkmSfdxgqONsrBN/rLdk00USADrMg=="], - - "expo-build-properties": ["expo-build-properties@1.0.9", "", { "dependencies": { "ajv": "^8.11.0", "semver": "^7.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-2icttCy3OPTk/GWIFt+vwA+0hup53jnmYb7JKRbvNvrrOrz+WblzpeoiaOleI2dYG/vjwpNO8to8qVyKhYJtrQ=="], - - "expo-constants": ["expo-constants@18.0.9", "", { "dependencies": { "@expo/config": "~12.0.9", "@expo/env": "~2.0.7" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-sqoXHAOGDcr+M9NlXzj1tGoZyd3zxYDy215W6E0Z0n8fgBaqce9FAYQE2bu5X4G629AYig5go7U6sQz7Pjcm8A=="], - - "expo-file-system": ["expo-file-system@19.0.16", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-9Ee6HpcUEfO7dOet/on9yAg7ysegBua35Q0oGrJzoRc+xW6IlTxoSFbmK8QhjA3MZpkukP3DhaiYENYOzkw9SQ=="], - - "expo-font": ["expo-font@14.0.8", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-bTUHaJWRZ7ywP8dg3f+wfOwv6RwMV3mWT2CDUIhsK70GjNGlCtiWOCoHsA5Od/esPaVxqc37cCBvQGQRFStRlA=="], - - "expo-haptics": ["expo-haptics@15.0.7", "", { "peerDependencies": { "expo": "*" } }, "sha512-7flWsYPrwjJxZ8x82RiJtzsnk1Xp9ahnbd9PhCy3NnsemyMApoWIEUr4waPqFr80DtiLZfhD9VMLL1CKa8AImQ=="], - - "expo-image": ["expo-image@3.0.8", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-L83fTHVjvE5hACxUXPk3dpABteI/IypeqxKMeOAAcT2eB/jbqT53ddsYKEvKAP86eoByQ7+TCtw9AOUizEtaTQ=="], - - "expo-keep-awake": ["expo-keep-awake@15.0.7", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-CgBNcWVPnrIVII5G54QDqoE125l+zmqR4HR8q+MQaCfHet+dYpS5vX5zii/RMayzGN4jPgA4XYIQ28ePKFjHoA=="], - - "expo-linking": ["expo-linking@8.0.8", "", { "dependencies": { "expo-constants": "~18.0.8", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MyeMcbFDKhXh4sDD1EHwd0uxFQNAc6VCrwBkNvvvufUsTYFq3glTA9Y8a+x78CPpjNqwNAamu74yIaIz7IEJyg=="], - - "expo-modules-autolinking": ["expo-modules-autolinking@3.0.14", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "glob": "^10.4.2", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-/qh1ru2kGPOycGvE9dXEKJZbPmYA5U5UcAlWWFbcq9+VhhWdZWZ0zs7V2JCdl+OvpBDo1y9WbqPP5VHQSYqT+Q=="], - - "expo-modules-core": ["expo-modules-core@3.0.20", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-AnC7VG8k8ZAAKoNFP5zyCiTlwppp6U3A/z63KtuSjMWlxn5w45FOf2LuyF1SNUqkiARdckuPVNvLGO/I/5vkrg=="], - - "expo-router": ["expo-router@6.0.10", "", { "dependencies": { "@expo/metro-runtime": "^6.1.2", "@expo/schema-utils": "^0.1.7", "@radix-ui/react-slot": "1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/native": "^7.1.8", "@react-navigation/native-stack": "^7.3.16", "client-only": "^0.0.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "expo-server": "^1.0.0", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", "nanoid": "^3.3.8", "query-string": "^7.1.3", "react-fast-compare": "^3.2.2", "react-native-is-edge-to-edge": "^1.1.6", "semver": "~7.6.3", "server-only": "^0.0.1", "sf-symbols-typescript": "^2.1.0", "shallowequal": "^1.1.0", "use-latest-callback": "^0.2.1", "vaul": "^1.1.2" }, "peerDependencies": { "@react-navigation/drawer": "^7.5.0", "@testing-library/react-native": ">= 12.0.0", "expo": "*", "expo-constants": "^18.0.9", "expo-linking": "^8.0.8", "react": "*", "react-dom": "*", "react-native": "*", "react-native-gesture-handler": "*", "react-native-reanimated": "*", "react-native-safe-area-context": ">= 5.4.0", "react-native-screens": "*", "react-native-web": "*", "react-server-dom-webpack": ">= 19.0.0" }, "optionalPeers": ["@react-navigation/drawer", "@testing-library/react-native", "react-dom", "react-native-gesture-handler", "react-native-reanimated", "react-native-web", "react-server-dom-webpack"] }, "sha512-QdMvNgjpH5L1ndE2KcYk14CjfulQTZNJNjM24/NigF+2cwkE7Ixdkw2EdpslcXPCgwcoJmvJIJtySsGhoPTNdg=="], - - "expo-server": ["expo-server@1.0.0", "", {}, "sha512-fAAI0ZXxayc2Rt5KfQjULv+TFreuLRZ+hdpc5TxZJ7CDpW1ZIqaVzELHh1rYTRVEBDFDiCBXtioS9WWTEAX+fg=="], - - "expo-splash-screen": ["expo-splash-screen@31.0.10", "", { "dependencies": { "@expo/prebuild-config": "^54.0.3" }, "peerDependencies": { "expo": "*" } }, "sha512-i6g9IK798mae4yvflstQ1HkgahIJ6exzTCTw4vEdxV0J2SwiW3Tj+CwRjf0te7Zsb+7dDQhBTmGZwdv00VER2A=="], - - "expo-status-bar": ["expo-status-bar@3.0.8", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-L248XKPhum7tvREoS1VfE0H6dPCaGtoUWzRsUv7hGKdiB4cus33Rc0sxkWkoQ77wE8stlnUlL5lvmT0oqZ3ZBw=="], - - "expo-symbols": ["expo-symbols@1.0.7", "", { "dependencies": { "sf-symbols-typescript": "^2.0.0" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-ZqFUeTXbwO6BrE00n37wTXYfJmsjFrfB446jeB9k9w7aA8a6eugNUIzNsUIUfbFWoOiY4wrGmpLSLPBwk4PH+g=="], - - "expo-system-ui": ["expo-system-ui@6.0.7", "", { "dependencies": { "@react-native/normalize-colors": "0.81.4", "debug": "^4.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-NT+/r/BOg08lFI9SZO2WFi9X1ZmawkVStknioWzQq6Mt4KinoMS6yl3eLbyOLM3LoptN13Ywfo4W5KHA6TV9Ow=="], - - "expo-web-browser": ["expo-web-browser@15.0.8", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-gn+Y2ABQr6/EvFN/XSjTuzwsSPLU1vNVVV0wNe4xXkcSnYGdHxt9kHxs9uLfoCyPByoaGF4VxzAhHIMI7yDcSg=="], - - "exponential-backoff": ["exponential-backoff@3.1.2", "", {}, "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA=="], - - "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-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], - - "fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="], - - "fbjs": ["fbjs@3.0.5", "", { "dependencies": { "cross-fetch": "^3.1.5", "fbjs-css-vars": "^1.0.0", "loose-envify": "^1.0.0", "object-assign": "^4.1.0", "promise": "^7.1.1", "setimmediate": "^1.0.5", "ua-parser-js": "^1.0.35" } }, "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg=="], - - "fbjs-css-vars": ["fbjs-css-vars@1.0.2", "", {}, "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "filter-obj": ["filter-obj@1.1.0", "", {}, "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="], - - "finalhandler": ["finalhandler@1.1.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", "parseurl": "~1.3.3", "statuses": "~1.5.0", "unpipe": "~1.0.0" } }, "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA=="], - - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - - "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], - - "flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - - "fontfaceobserver": ["fontfaceobserver@2.3.0", "", {}, "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg=="], - - "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], - - "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], - - "freeport-async": ["freeport-async@2.0.0", "", {}, "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ=="], - - "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - - "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], - - "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], - - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], - - "get-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], - - "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], - - "getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="], - - "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=="], - - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "global-dirs": ["global-dirs@0.1.1", "", { "dependencies": { "ini": "^1.3.4" } }, "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg=="], - - "globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="], - - "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], - - "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=="], - - "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], - - "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], - - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], - - "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="], - - "hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="], - - "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], - - "hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], - - "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], - - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - - "hyphenate-style-name": ["hyphenate-style-name@1.1.0", "", {}, "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="], - - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="], - - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - - "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - - "inline-style-prefixer": ["inline-style-prefixer@7.0.1", "", { "dependencies": { "css-in-js-utils": "^3.1.0" } }, "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw=="], - - "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], - - "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], - - "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], - - "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], - - "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], - - "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], - - "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], - - "is-bun-module": ["is-bun-module@2.0.0", "", { "dependencies": { "semver": "^7.7.1" } }, "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ=="], - - "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], - - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - - "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], - - "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], - - "is-directory": ["is-directory@0.3.1", "", {}, "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw=="], - - "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-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "is-generator-function": ["is-generator-function@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], - - "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], - - "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], - - "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], - - "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], - - "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], - - "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], - - "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], - - "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], - - "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], - - "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], - - "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - - "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "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=="], - - "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], - - "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], - - "jest-environment-node": ["jest-environment-node@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw=="], - - "jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], - - "jest-haste-map": ["jest-haste-map@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA=="], - - "jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - - "jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="], - - "jest-regex-util": ["jest-regex-util@29.6.3", "", {}, "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg=="], - - "jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], - - "jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="], - - "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=="], - - "jimp-compact": ["jimp-compact@0.16.1", "", {}, "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - - "jsc-safe-url": ["jsc-safe-url@0.2.4", "", {}, "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q=="], - - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - - "json-parse-better-errors": ["json-parse-better-errors@1.0.2", "", {}, "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="], - - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - - "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], - - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - - "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=="], - - "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], - - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - - "lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="], - - "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], - - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], - - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], - - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], - - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], - - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], - - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], - - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], - - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], - - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], - - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], - - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], - - "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], - - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - - "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=="], - - "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - - "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="], - - "marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="], - - "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.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-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=="], - - "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-cache-key": ["metro-cache-key@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-ZUs+GD5CNeDLxx5UUWmfg26IL+Dnbryd+TLqTlZnDEgehkIa11kUSvgF92OFfJhONeXzV4rZDRGNXoo6JT+8Gg=="], - - "metro-config": ["metro-config@0.83.1", "", { "dependencies": { "connect": "^3.6.5", "cosmiconfig": "^5.0.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.1", "metro-cache": "0.83.1", "metro-core": "0.83.1", "metro-runtime": "0.83.1" } }, "sha512-HJhpZx3wyOkux/jeF1o7akFJzZFdbn6Zf7UQqWrvp7gqFqNulQ8Mju09raBgPmmSxKDl4LbbNeigkX0/nKY1QA=="], - - "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-file-map": ["metro-file-map@0.83.1", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-Yu429lnexKl44PttKw3nhqgmpBR+6UQ/tRaYcxPeEShtcza9DWakCn7cjqDTQZtWR2A8xSNv139izJMyQ4CG+w=="], - - "metro-minify-terser": ["metro-minify-terser@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-kmooOxXLvKVxkh80IVSYO4weBdJDhCpg5NSPkjzzAnPJP43u6+usGXobkTWxxrAlq900bhzqKek4pBsUchlX6A=="], - - "metro-resolver": ["metro-resolver@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-t8j46kiILAqqFS5RNa+xpQyVjULxRxlvMidqUswPEk5nQVNdlJslqizDm/Et3v/JKwOtQGkYAQCHxP1zGStR/g=="], - - "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-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.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=="], - - "metro-transform-worker": ["metro-transform-worker@0.83.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "metro": "0.83.1", "metro-babel-transformer": "0.83.1", "metro-cache": "0.83.1", "metro-cache-key": "0.83.1", "metro-minify-terser": "0.83.1", "metro-source-map": "0.83.1", "metro-transform-plugins": "0.83.1", "nullthrows": "^1.1.1" } }, "sha512-owCrhPyUxdLgXEEEAL2b14GWTPZ2zYuab1VQXcfEy0sJE71iciD7fuMcrngoufh7e7UHDZ56q4ktXg8wgiYA1Q=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], - - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - - "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="], - - "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "napi-postinstall": ["napi-postinstall@0.3.3", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow=="], - - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - - "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - - "nested-error-stacks": ["nested-error-stacks@2.0.1", "", {}, "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A=="], - - "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=="], - - "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], - - "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], - - "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - - "npm-package-arg": ["npm-package-arg@11.0.3", "", { "dependencies": { "hosted-git-info": "^7.0.0", "proc-log": "^4.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^5.0.0" } }, "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw=="], - - "nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="], - - "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=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], - - "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], - - "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], - - "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], - - "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], - - "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "on-headers": ["on-headers@1.1.0", "", {}, "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="], - - "open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], - - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - - "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=="], - - "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], - - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], - - "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], - - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - - "parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="], - - "parse-png": ["parse-png@2.1.0", "", { "dependencies": { "pngjs": "^3.3.0" } }, "sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ=="], - - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - - "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - - "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=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@3.0.1", "", {}, "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag=="], - - "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], - - "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], - - "pngjs": ["pngjs@3.4.0", "", {}, "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="], - - "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - - "postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="], - - "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], - - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - - "pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], - - "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=="], - - "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], - - "promise": ["promise@8.3.0", "", { "dependencies": { "asap": "~2.0.6" } }, "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg=="], - - "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=="], - - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "qrcode-terminal": ["qrcode-terminal@0.11.0", "", { "bin": { "qrcode-terminal": "./bin/qrcode-terminal.js" } }, "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ=="], - - "query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="], - - "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=="], - - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], - - "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], - - "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.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], - - "react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="], - - "react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="], - - "react-is": ["react-is@19.1.1", "", {}, "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA=="], - - "react-native": ["react-native@0.81.4", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.4", "@react-native/codegen": "0.81.4", "@react-native/community-cli-plugin": "0.81.4", "@react-native/gradle-plugin": "0.81.4", "@react-native/js-polyfills": "0.81.4", "@react-native/normalize-colors": "0.81.4", "@react-native/virtualized-lists": "0.81.4", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.29.1", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.83.1", "metro-source-map": "^0.83.1", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.26.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.0", "react": "^19.1.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-bt5bz3A/+Cv46KcjV0VQa+fo7MKxs17RCcpzjftINlen4ZDUl0I6Ut+brQ2FToa5oD0IB0xvQHfmsg2EDqsZdQ=="], - - "react-native-gesture-handler": ["react-native-gesture-handler@2.28.0", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A=="], - - "react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.2.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q=="], - - "react-native-nitro-modules": ["react-native-nitro-modules@0.35.6", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-3Cb7s+O5tpZ6RdIiPOB/wi3IMfBxD6tl6VDF8gJ5zvM/BEGTWxwMMLjzmWmsYPKekdbYBznF6qp2d2SxixPy8g=="], - - "react-native-reanimated": ["react-native-reanimated@4.1.2", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1", "semver": "7.7.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*", "react-native-worklets": ">=0.5.0" } }, "sha512-qzmQiFrvjm62pRBcj97QI9Xckc3EjgHQoY1F2yjktd0kpjhoyePeuTEXjYRCAVIy7IV/1cfeSup34+zFThFoHQ=="], - - "react-native-safe-area-context": ["react-native-safe-area-context@5.6.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-/wJE58HLEAkATzhhX1xSr+fostLsK8Q97EfpfMDKo8jlOc1QKESSX/FQrhk7HhQH/2uSaox4Y86sNaI02kteiA=="], - - "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-web": ["react-native-web@0.21.1", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-BeNsgwwe4AXUFPAoFU+DKjJ+CVQa3h54zYX77p7GVZrXiiNo3vl03WYDYVEy5R2J2HOPInXtQZB5gmj3vuzrKg=="], - - "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=="], - - "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], - - "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="], - - "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - - "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=="], - - "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], - - "regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], - - "regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.0", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA=="], - - "regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="], - - "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], - - "regexpu-core": ["regexpu-core@6.2.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.0", "regjsgen": "^0.8.0", "regjsparser": "^0.12.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" } }, "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA=="], - - "regjsgen": ["regjsgen@0.8.0", "", {}, "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q=="], - - "regjsparser": ["regjsparser@0.12.0", "", { "dependencies": { "jsesc": "~3.0.2" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ=="], - - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - - "requireg": ["requireg@0.2.2", "", { "dependencies": { "nested-error-stacks": "~2.0.1", "rc": "~1.2.7", "resolve": "~1.7.1" } }, "sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg=="], - - "resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], - - "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - - "resolve-global": ["resolve-global@1.0.0", "", { "dependencies": { "global-dirs": "^0.1.1" } }, "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw=="], - - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - - "resolve-workspace-root": ["resolve-workspace-root@2.0.0", "", {}, "sha512-IsaBUZETJD5WsI11Wt8PKHwaIe45or6pwNc8yflvLJ4DWtImK9kuLoH5kUva/2Mmx/RdIyr4aONNSa2v9LTJsw=="], - - "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=="], - - "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-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], - - "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], - - "sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="], - - "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - - "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=="], - - "serialize-error": ["serialize-error@2.1.0", "", {}, "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw=="], - - "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], - - "server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="], - - "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], - - "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], - - "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], - - "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "sf-symbols-typescript": ["sf-symbols-typescript@2.1.0", "", {}, "sha512-ezT7gu/SHTPIOEEoG6TF+O0m5eewl0ZDAO4AtdBi5HjsrUI6JdCG17+Q8+aKp0heM06wZKApRCn5olNbs0Wb/A=="], - - "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=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], - - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "simple-plist": ["simple-plist@1.3.1", "", { "dependencies": { "bplist-creator": "0.1.0", "bplist-parser": "0.3.1", "plist": "^3.0.5" } }, "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw=="], - - "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], - - "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - - "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="], - - "source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - - "split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="], - - "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], - - "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], - - "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], - - "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], - - "stacktrace-parser": ["stacktrace-parser@0.1.11", "", { "dependencies": { "type-fest": "^0.7.1" } }, "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg=="], - - "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], - - "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], - - "stream-buffers": ["stream-buffers@2.2.0", "", {}, "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg=="], - - "strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="], - - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], - - "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], - - "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], - - "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], - - "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], - - "strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="], - - "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], - - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - - "structured-headers": ["structured-headers@0.4.1", "", {}, "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg=="], - - "styleq": ["styleq@0.1.3", "", {}, "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA=="], - - "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="], - - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "supports-hyperlinks": ["supports-hyperlinks@2.3.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - - "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="], - - "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=="], - - "terser": ["terser@5.43.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg=="], - - "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], - - "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], - - "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - - "throat": ["throat@5.0.0", "", {}, "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA=="], - - "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], - - "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - - "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], - - "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], - - "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - - "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], - - "type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="], - - "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], - - "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], - - "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], - - "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "ua-parser-js": ["ua-parser-js@1.0.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug=="], - - "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - - "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], - - "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], - - "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], - - "unicode-match-property-ecmascript": ["unicode-match-property-ecmascript@2.0.0", "", { "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" } }, "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q=="], - - "unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.0", "", {}, "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg=="], - - "unicode-property-aliases-ecmascript": ["unicode-property-aliases-ecmascript@2.1.0", "", {}, "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w=="], - - "unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="], - - "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], - - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - - "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "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-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], - - "use-latest-callback": ["use-latest-callback@0.2.4", "", { "peerDependencies": { "react": ">=16.8" } }, "sha512-LS2s2n1usUUnDq4oVh1ca6JFX9uSqUncTfAm44WMg0v6TxL7POUTk1B044NH8TeLkFbNajIsgDHcgNpNzZucdg=="], - - "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.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-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], - - "use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], - - "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - - "uuid": ["uuid@7.0.3", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="], - - "validate-npm-package-name": ["validate-npm-package-name@5.0.1", "", {}, "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - - "vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="], - - "vlq": ["vlq@1.0.1", "", {}, "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w=="], - - "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], - - "warn-once": ["warn-once@0.1.1", "", {}, "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q=="], - - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], - - "webidl-conversions": ["webidl-conversions@5.0.0", "", {}, "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA=="], - - "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], - - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - - "whatwg-url-without-unicode": ["whatwg-url-without-unicode@8.0.0-3", "", { "dependencies": { "buffer": "^5.4.3", "punycode": "^2.1.1", "webidl-conversions": "^5.0.0" } }, "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], - - "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], - - "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], - - "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], - - "wonka": ["wonka@6.3.5", "", {}, "sha512-SSil+ecw6B4/Dm7Pf2sAshKQ5hWFvfyGlfPbEd6A14dOH6VDjrmbY86u6nZvy9omGwwIPFR8V41+of1EezgoUw=="], - - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="], - - "ws": ["ws@6.2.3", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA=="], - - "xcode": ["xcode@3.0.1", "", { "dependencies": { "simple-plist": "^1.1.0", "uuid": "^7.0.3" } }, "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA=="], - - "xml2js": ["xml2js@0.6.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w=="], - - "xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], - - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - - "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="], - - "@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], - - "@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=="], - - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - - "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - - "@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/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], - - "@expo/cli/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "@expo/cli/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - - "@expo/config/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], - - "@expo/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=="], - - "@expo/config/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "@expo/config-plugins/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/config-plugins/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "@expo/devcert/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "@expo/devcert/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/fingerprint/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/fingerprint/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@expo/fingerprint/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "@expo/image-utils/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "@expo/json-file/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], - - "@expo/mcp-tunnel/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - - "@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=="], - - "@expo/metro-config/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@expo/prebuild-config/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "@expo/xcpretty/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], - - "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], - - "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - - "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - - "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - - "@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - - "@istanbuljs/load-nyc-config/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], - - "@istanbuljs/load-nyc-config/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=="], - - "@radix-ui/react-collection/@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=="], - - "@radix-ui/react-dialog/@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=="], - - "@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-plugin/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], - - "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "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=="], - - "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=="], - - "compression/negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], - - "connect/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "cosmiconfig/import-fresh": ["import-fresh@2.0.0", "", { "dependencies": { "caller-path": "^2.0.0", "resolve-from": "^3.0.0" } }, "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg=="], - - "cosmiconfig/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=="], - - "error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], - - "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "eslint-import-resolver-node/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], - - "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "expo-build-properties/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "expo-build-properties/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "expo-modules-autolinking/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], - - "expo-modules-autolinking/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-router/@expo/metro-runtime": ["@expo/metro-runtime@6.1.2", "", { "dependencies": { "anser": "^1.4.9", "pretty-format": "^29.7.0", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-dom": "*", "react-native": "*" }, "optionalPeers": ["react-dom"] }, "sha512-nvM+Qv45QH7pmYvP8JB1G8JpScrWND3KrMA6ZKe62cwwNiX/BjHU28Ear0v/4bQWXlOY0mv6B8CDIm8JxXde9g=="], - - "expo-router/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], - - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "fbjs/promise": ["promise@7.3.1", "", { "dependencies": { "asap": "~2.0.3" } }, "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg=="], - - "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=="], - - "finalhandler/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="], - - "finalhandler/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], - - "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - - "hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - - "is-bun-module/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-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - - "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=="], - - "metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], - - "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=="], - - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "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=="], - - "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - - "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - - "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=="], - - "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-web/@react-native/normalize-colors": ["@react-native/normalize-colors@0.74.89", "", {}, "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg=="], - - "react-native-web/memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="], - - "react-native-worklets/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "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/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "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=="], - - "simple-plist/bplist-parser": ["bplist-parser@0.3.1", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA=="], - - "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - - "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], - - "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], - - "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - - "sucrase/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=="], - - "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - - "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - - "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], - - "whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - - "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "write-file-atomic/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], - - "@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=="], - - "@expo/cli/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "@expo/config-plugins/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@expo/config/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@expo/devcert/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@expo/fingerprint/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "@expo/metro-config/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], - - "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - - "@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - - "@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "cosmiconfig/import-fresh/resolve-from": ["resolve-from@3.0.0", "", {}, "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw=="], - - "cosmiconfig/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - - "expo-build-properties/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "expo-modules-autolinking/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "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=="], - - "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=="], - - "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], - - "sucrase/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@babel/highlight/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - - "@expo/config-plugins/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "@expo/config/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "@expo/devcert/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - - "expo-modules-autolinking/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "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=="], - - "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=="], - - "serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "sucrase/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - - "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - - "log-symbols/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - - "ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - } -} diff --git a/libraries/react-native-iap/example-expo/components/AndroidOneTimeOfferDetails.tsx b/libraries/react-native-iap/example-expo/components/AndroidOneTimeOfferDetails.tsx deleted file mode 100644 index d7161cdf..00000000 --- a/libraries/react-native-iap/example-expo/components/AndroidOneTimeOfferDetails.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import {View, Text, StyleSheet} from 'react-native'; -import type {ProductAndroidOneTimePurchaseOfferDetail} from 'react-native-iap'; - -type AndroidOneTimeOfferDetailsProps = { - offers: ProductAndroidOneTimePurchaseOfferDetail[]; -}; - -/** - * Shared component for displaying Android one-time purchase offer details. - * Used in PurchaseFlow, AllProducts, and SubscriptionFlow screens. - */ -export default function AndroidOneTimeOfferDetails({ - offers, -}: AndroidOneTimeOfferDetailsProps) { - if (!offers || offers.length === 0) { - return null; - } - - return ( - - - One-Time Purchase Offers ({offers.length}) - - {offers.map( - (offer: ProductAndroidOneTimePurchaseOfferDetail, index: number) => ( - - - Offer {index + 1} - {offer.offerId ? ` (${offer.offerId})` : ''} - - - Price: - - {offer.formattedPrice} ({offer.priceAmountMicros} micros) - - - {offer.fullPriceMicros && ( - <> - Full Price: - - {offer.fullPriceMicros} micros - - - )} - - {offer.discountDisplayInfo && ( - <> - Discount: - - {offer.discountDisplayInfo.percentageDiscount - ? `${offer.discountDisplayInfo.percentageDiscount}% off` - : offer.discountDisplayInfo.discountAmount - ? `${offer.discountDisplayInfo.discountAmount.formattedDiscountAmount} off` - : 'N/A'} - - - )} - - {offer.limitedQuantityInfo && ( - <> - Limited Quantity: - - {offer.limitedQuantityInfo.remainingQuantity} /{' '} - {offer.limitedQuantityInfo.maximumQuantity} remaining - - - )} - - {offer.validTimeWindow && ( - <> - Valid Window: - - {new Date( - Number(offer.validTimeWindow.startTimeMillis), - ).toLocaleDateString()}{' '} - -{' '} - {new Date( - Number(offer.validTimeWindow.endTimeMillis), - ).toLocaleDateString()} - - - )} - - {offer.preorderDetailsAndroid && ( - <> - Pre-order Release: - - {new Date( - Number( - offer.preorderDetailsAndroid.preorderReleaseTimeMillis, - ), - ).toLocaleDateString()} - - - )} - - {offer.rentalDetailsAndroid && ( - <> - Rental: - - Period: {offer.rentalDetailsAndroid.rentalPeriod} - - - )} - - {offer.offerTags.length > 0 && ( - <> - Tags: - - {offer.offerTags.join(', ')} - - - )} - - Offer Token: - - {offer.offerToken} - - - ), - )} - - ); -} - -const styles = StyleSheet.create({ - offersSection: { - marginTop: 20, - paddingTop: 15, - borderTopWidth: 1, - borderTopColor: '#e0e0e0', - }, - offersSectionTitle: { - fontSize: 16, - fontWeight: '700', - color: '#333', - marginBottom: 12, - }, - offerCard: { - backgroundColor: '#f8f9fa', - borderRadius: 8, - padding: 12, - marginBottom: 10, - borderLeftWidth: 3, - borderLeftColor: '#007AFF', - }, - offerTitle: { - fontSize: 14, - fontWeight: '700', - color: '#007AFF', - marginBottom: 8, - }, - offerLabel: { - fontSize: 11, - color: '#666', - marginTop: 6, - fontWeight: '600', - }, - offerValue: { - fontSize: 13, - color: '#333', - marginTop: 2, - }, - offerValueDiscount: { - fontSize: 13, - color: '#E53935', - marginTop: 2, - fontWeight: '600', - }, - offerToken: { - fontSize: 10, - color: '#999', - fontFamily: 'monospace', - }, -}); diff --git a/libraries/react-native-iap/example-expo/components/Collapsible.tsx b/libraries/react-native-iap/example-expo/components/Collapsible.tsx deleted file mode 100644 index 60d3fc76..00000000 --- a/libraries/react-native-iap/example-expo/components/Collapsible.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import type {PropsWithChildren} from 'react'; -import {useState} from 'react'; -import {StyleSheet, TouchableOpacity} from 'react-native'; - -import {ThemedText} from '@/components/ThemedText'; -import {ThemedView} from '@/components/ThemedView'; -import {IconSymbol} from '@/components/ui/IconSymbol'; -import {Colors} from '@/constants/Colors'; -import {useColorScheme} from '@/hooks/useColorScheme'; - -export function Collapsible({ - children, - title, -}: PropsWithChildren & {title: string}) { - const [isOpen, setIsOpen] = useState(false); - const theme = useColorScheme() ?? 'light'; - - return ( - - setIsOpen((value) => !value)} - activeOpacity={0.8} - > - - - {title} - - {isOpen && {children}} - - ); -} - -const styles = StyleSheet.create({ - heading: { - flexDirection: 'row', - alignItems: 'center', - gap: 6, - }, - content: { - marginTop: 6, - marginLeft: 24, - }, -}); diff --git a/libraries/react-native-iap/example-expo/components/ExternalLink.tsx b/libraries/react-native-iap/example-expo/components/ExternalLink.tsx deleted file mode 100644 index 335cd243..00000000 --- a/libraries/react-native-iap/example-expo/components/ExternalLink.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type {Href} from 'expo-router'; -import {Link} from 'expo-router'; -import {openBrowserAsync} from 'expo-web-browser'; -import {type ComponentProps} from 'react'; -import {Platform} from 'react-native'; - -type Props = Omit, 'href'> & {href: Href & string}; - -export function ExternalLink({href, ...rest}: Props) { - return ( - { - if (Platform.OS !== 'web') { - // Prevent the default behavior of linking to the default browser on native. - event.preventDefault(); - // Open the link in an in-app browser. - await openBrowserAsync(href); - } - }} - /> - ); -} diff --git a/libraries/react-native-iap/example-expo/components/HapticTab.tsx b/libraries/react-native-iap/example-expo/components/HapticTab.tsx deleted file mode 100644 index a3a17cf6..00000000 --- a/libraries/react-native-iap/example-expo/components/HapticTab.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type {BottomTabBarButtonProps} from '@react-navigation/bottom-tabs'; -import {PlatformPressable} from '@react-navigation/elements'; -import * as Haptics from 'expo-haptics'; - -export function HapticTab(props: BottomTabBarButtonProps) { - return ( - { - if (process.env.EXPO_OS === 'ios') { - // Add a soft haptic feedback when pressing down on the tabs. - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - } - props.onPressIn?.(ev); - }} - /> - ); -} diff --git a/libraries/react-native-iap/example-expo/components/HelloWave.tsx b/libraries/react-native-iap/example-expo/components/HelloWave.tsx deleted file mode 100644 index 2f46fbdf..00000000 --- a/libraries/react-native-iap/example-expo/components/HelloWave.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import {useEffect} from 'react'; -import {StyleSheet} from 'react-native'; -import Animated, { - useAnimatedStyle, - useSharedValue, - withRepeat, - withSequence, - withTiming, -} from 'react-native-reanimated'; - -import {ThemedText} from '@/components/ThemedText'; - -export function HelloWave() { - const rotationAnimation = useSharedValue(0); - - useEffect(() => { - rotationAnimation.value = withRepeat( - withSequence( - withTiming(25, {duration: 150}), - withTiming(0, {duration: 150}), - ), - 4, // Run the animation 4 times - ); - }, [rotationAnimation]); - - const animatedStyle = useAnimatedStyle(() => ({ - transform: [{rotate: `${rotationAnimation.value}deg`}], - })); - - return ( - - 👋 - - ); -} - -const styles = StyleSheet.create({ - text: { - fontSize: 28, - lineHeight: 32, - marginTop: -6, - }, -}); diff --git a/libraries/react-native-iap/example-expo/components/Loading.tsx b/libraries/react-native-iap/example-expo/components/Loading.tsx deleted file mode 100644 index d78de66d..00000000 --- a/libraries/react-native-iap/example-expo/components/Loading.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { - View, - ActivityIndicator, - Text, - StyleSheet, - Platform, -} from 'react-native'; - -type LoadingProps = { - message?: string; -}; - -export default function Loading({ - message = 'Connecting to Store...', -}: LoadingProps) { - return ( - - - {message} - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: '#fff', - }, - message: { - marginTop: 12, - fontSize: 14, - color: '#666', - }, -}); diff --git a/libraries/react-native-iap/example-expo/components/ParallaxScrollView.tsx b/libraries/react-native-iap/example-expo/components/ParallaxScrollView.tsx deleted file mode 100644 index 4315c50c..00000000 --- a/libraries/react-native-iap/example-expo/components/ParallaxScrollView.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import type {PropsWithChildren, ReactElement} from 'react'; -import {StyleSheet} from 'react-native'; -import Animated, { - interpolate, - useAnimatedRef, - useAnimatedStyle, - useScrollViewOffset, -} from 'react-native-reanimated'; - -import {ThemedView} from '@/components/ThemedView'; -import {useBottomTabOverflow} from '@/components/ui/TabBarBackground'; -import {useColorScheme} from '@/hooks/useColorScheme'; - -const HEADER_HEIGHT = 250; - -type Props = PropsWithChildren<{ - headerImage: ReactElement; - headerBackgroundColor: {dark: string; light: string}; -}>; - -export default function ParallaxScrollView({ - children, - headerImage, - headerBackgroundColor, -}: Props) { - const colorScheme = useColorScheme() ?? 'light'; - const scrollRef = useAnimatedRef(); - const scrollOffset = useScrollViewOffset(scrollRef); - const bottom = useBottomTabOverflow(); - const headerAnimatedStyle = useAnimatedStyle(() => { - return { - transform: [ - { - translateY: interpolate( - scrollOffset.value, - [-HEADER_HEIGHT, 0, HEADER_HEIGHT], - [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75], - ), - }, - { - scale: interpolate( - scrollOffset.value, - [-HEADER_HEIGHT, 0, HEADER_HEIGHT], - [2, 1, 1], - ), - }, - ], - }; - }); - - return ( - - - - {headerImage} - - {children} - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - header: { - height: HEADER_HEIGHT, - overflow: 'hidden', - }, - content: { - flex: 1, - padding: 32, - gap: 16, - overflow: 'hidden', - }, -}); diff --git a/libraries/react-native-iap/example-expo/components/PurchaseDetails.tsx b/libraries/react-native-iap/example-expo/components/PurchaseDetails.tsx deleted file mode 100644 index 3eb103df..00000000 --- a/libraries/react-native-iap/example-expo/components/PurchaseDetails.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import {useMemo} from 'react'; -import {StyleSheet, Text, View} from 'react-native'; -import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; -import type {Purchase} from 'react-native-iap'; -import {buildPurchaseRows} from '../utils/buildPurchaseRows'; - -type PurchaseDetailsProps = { - purchase: Purchase; - containerStyle?: StyleProp; - rowStyle?: StyleProp; - labelStyle?: StyleProp; - valueStyle?: StyleProp; -}; - -function PurchaseDetails({ - purchase, - containerStyle, - rowStyle, - labelStyle, - valueStyle, -}: PurchaseDetailsProps) { - const rows = useMemo(() => buildPurchaseRows(purchase), [purchase]); - - if (rows.length === 0) { - return null; - } - - return ( - - {rows.map((row) => ( - - {row.label} - {row.value} - - ))} - - ); -} - -const styles = StyleSheet.create({ - container: { - gap: 8, - }, - row: { - flexDirection: 'column', - gap: 4, - paddingVertical: 6, - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: '#e1e7ef', - }, - label: { - fontSize: 12, - color: '#5f6470', - fontWeight: '600', - }, - value: { - fontSize: 14, - color: '#1a1f36', - }, -}); - -export default PurchaseDetails; diff --git a/libraries/react-native-iap/example-expo/components/PurchaseSummaryRow.tsx b/libraries/react-native-iap/example-expo/components/PurchaseSummaryRow.tsx deleted file mode 100644 index d256ff1c..00000000 --- a/libraries/react-native-iap/example-expo/components/PurchaseSummaryRow.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'; -import type {StyleProp, ViewStyle} from 'react-native'; -import type {Purchase} from 'react-native-iap'; - -const platformLabel = (platform?: string | null): string => { - if (!platform) return 'unknown'; - return platform.toString().toLowerCase(); -}; - -const platformStyle = (platform?: string | null) => { - const normalized = platformLabel(platform); - switch (normalized) { - case 'ios': - return styles.badgeIOS; - case 'android': - return styles.badgeAndroid; - default: - return styles.badgeUnknown; - } -}; - -type PurchaseWithMaybeTransactionId = Purchase & { - transactionId?: string | null; -}; - -const resolveTransactionId = ( - purchase: PurchaseWithMaybeTransactionId, -): string => { - const transactionId = purchase.transactionId; - if (typeof transactionId === 'string' && transactionId.length > 0) { - return transactionId; - } - return purchase.id; -}; - -type Props = { - purchase: Purchase; - onPress?: () => void; - style?: StyleProp; -}; - -function PurchaseSummaryRow({purchase, onPress, style}: Props) { - const platform = platformLabel(purchase.platform); - const transactionId = resolveTransactionId(purchase); - - return ( - - - - {purchase.productId} - - - Transaction: {transactionId || 'N/A'} - - - - {platform} - - - ); -} - -const styles = StyleSheet.create({ - row: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingVertical: 12, - paddingHorizontal: 16, - borderRadius: 12, - backgroundColor: '#f7f9fc', - borderWidth: 1, - borderColor: '#e1e7ef', - marginBottom: 12, - }, - rowPressable: { - shadowColor: '#000', - shadowOffset: {width: 0, height: 1}, - shadowOpacity: 0.05, - shadowRadius: 2, - elevation: 1, - }, - infoContainer: { - flex: 1, - marginRight: 12, - gap: 4, - }, - productId: { - fontSize: 16, - fontWeight: '600', - color: '#1a1a1a', - }, - transactionId: { - fontSize: 13, - color: '#5f6470', - }, - badge: { - paddingHorizontal: 10, - paddingVertical: 4, - borderRadius: 999, - }, - badgeText: { - fontSize: 12, - fontWeight: '600', - color: '#fff', - textTransform: 'uppercase', - }, - badgeIOS: { - backgroundColor: '#007AFF', - }, - badgeAndroid: { - backgroundColor: '#3DDC84', - }, - badgeUnknown: { - backgroundColor: '#9E9E9E', - }, -}); - -export default PurchaseSummaryRow; diff --git a/libraries/react-native-iap/example-expo/components/ThemedText.tsx b/libraries/react-native-iap/example-expo/components/ThemedText.tsx deleted file mode 100644 index f67ae4bc..00000000 --- a/libraries/react-native-iap/example-expo/components/ThemedText.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import {StyleSheet, Text, type TextProps} from 'react-native'; - -import {useThemeColor} from '@/hooks/useThemeColor'; - -export type ThemedTextProps = TextProps & { - lightColor?: string; - darkColor?: string; - type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link'; -}; - -export function ThemedText({ - style, - lightColor, - darkColor, - type = 'default', - ...rest -}: ThemedTextProps) { - const color = useThemeColor({light: lightColor, dark: darkColor}, 'text'); - - return ( - - ); -} - -const styles = StyleSheet.create({ - default: { - fontSize: 16, - lineHeight: 24, - }, - defaultSemiBold: { - fontSize: 16, - lineHeight: 24, - fontWeight: '600', - }, - title: { - fontSize: 32, - fontWeight: 'bold', - lineHeight: 32, - }, - subtitle: { - fontSize: 20, - fontWeight: 'bold', - }, - link: { - lineHeight: 30, - fontSize: 16, - color: '#0a7ea4', - }, -}); diff --git a/libraries/react-native-iap/example-expo/components/ThemedView.tsx b/libraries/react-native-iap/example-expo/components/ThemedView.tsx deleted file mode 100644 index e6913792..00000000 --- a/libraries/react-native-iap/example-expo/components/ThemedView.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import {View, type ViewProps} from 'react-native'; - -import {useThemeColor} from '@/hooks/useThemeColor'; - -export type ThemedViewProps = ViewProps & { - lightColor?: string; - darkColor?: string; -}; - -export function ThemedView({ - style, - lightColor, - darkColor, - ...otherProps -}: ThemedViewProps) { - const backgroundColor = useThemeColor( - {light: lightColor, dark: darkColor}, - 'background', - ); - - return ; -} diff --git a/libraries/react-native-iap/example-expo/components/ui/IconSymbol.ios.tsx b/libraries/react-native-iap/example-expo/components/ui/IconSymbol.ios.tsx deleted file mode 100644 index a7b1f016..00000000 --- a/libraries/react-native-iap/example-expo/components/ui/IconSymbol.ios.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import type {SymbolViewProps, SymbolWeight} from 'expo-symbols'; -import {SymbolView} from 'expo-symbols'; -import type {StyleProp, ViewStyle} from 'react-native'; - -export function IconSymbol({ - name, - size = 24, - color, - style, - weight = 'regular', -}: { - name: SymbolViewProps['name']; - size?: number; - color: string; - style?: StyleProp; - weight?: SymbolWeight; -}) { - return ( - - ); -} diff --git a/libraries/react-native-iap/example-expo/components/ui/IconSymbol.tsx b/libraries/react-native-iap/example-expo/components/ui/IconSymbol.tsx deleted file mode 100644 index cad1aa24..00000000 --- a/libraries/react-native-iap/example-expo/components/ui/IconSymbol.tsx +++ /dev/null @@ -1,51 +0,0 @@ -// Fallback for using MaterialIcons on Android and web. - -import MaterialIcons from '@expo/vector-icons/MaterialIcons'; -import type {SymbolWeight, SymbolViewProps} from 'expo-symbols'; -import type {ComponentProps} from 'react'; -import type {OpaqueColorValue, StyleProp, TextStyle} from 'react-native'; - -type IconMapping = Record< - SymbolViewProps['name'], - ComponentProps['name'] ->; -type IconSymbolName = keyof typeof MAPPING; - -/** - * Add your SF Symbols to Material Icons mappings here. - * - see Material Icons in the [Icons Directory](https://icons.expo.fyi). - * - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app. - */ -const MAPPING = { - 'house.fill': 'home', - 'paperplane.fill': 'send', - 'chevron.left.forwardslash.chevron.right': 'code', - 'chevron.right': 'chevron-right', -} as IconMapping; - -/** - * An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web. - * This ensures a consistent look across platforms, and optimal resource usage. - * Icon `name`s are based on SF Symbols and require manual mapping to Material Icons. - */ -export function IconSymbol({ - name, - size = 24, - color, - style, -}: { - name: IconSymbolName; - size?: number; - color: string | OpaqueColorValue; - style?: StyleProp; - weight?: SymbolWeight; -}) { - return ( - - ); -} diff --git a/libraries/react-native-iap/example-expo/components/ui/TabBarBackground.ios.tsx b/libraries/react-native-iap/example-expo/components/ui/TabBarBackground.ios.tsx deleted file mode 100644 index 8ac7335f..00000000 --- a/libraries/react-native-iap/example-expo/components/ui/TabBarBackground.ios.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import {useBottomTabBarHeight} from '@react-navigation/bottom-tabs'; -import {BlurView} from 'expo-blur'; -import {StyleSheet} from 'react-native'; - -export default function BlurTabBarBackground() { - return ( - - ); -} - -export function useBottomTabOverflow() { - return useBottomTabBarHeight(); -} diff --git a/libraries/react-native-iap/example-expo/components/ui/TabBarBackground.tsx b/libraries/react-native-iap/example-expo/components/ui/TabBarBackground.tsx deleted file mode 100644 index 70d1c3c0..00000000 --- a/libraries/react-native-iap/example-expo/components/ui/TabBarBackground.tsx +++ /dev/null @@ -1,6 +0,0 @@ -// This is a shim for web and Android where the tab bar is generally opaque. -export default undefined; - -export function useBottomTabOverflow() { - return 0; -} diff --git a/libraries/react-native-iap/example-expo/constants/Colors.ts b/libraries/react-native-iap/example-expo/constants/Colors.ts deleted file mode 100644 index 14e67844..00000000 --- a/libraries/react-native-iap/example-expo/constants/Colors.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Below are the colors that are used in the app. The colors are defined in the light and dark mode. - * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc. - */ - -const tintColorLight = '#0a7ea4'; -const tintColorDark = '#fff'; - -export const Colors = { - light: { - text: '#11181C', - background: '#fff', - tint: tintColorLight, - icon: '#687076', - tabIconDefault: '#687076', - tabIconSelected: tintColorLight, - }, - dark: { - text: '#ECEDEE', - background: '#151718', - tint: tintColorDark, - icon: '#9BA1A6', - tabIconDefault: '#9BA1A6', - tabIconSelected: tintColorDark, - }, -}; diff --git a/libraries/react-native-iap/example-expo/constants/products.ts b/libraries/react-native-iap/example-expo/constants/products.ts deleted file mode 100644 index 2f27b995..00000000 --- a/libraries/react-native-iap/example-expo/constants/products.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Product ID constants used across the example apps and tests - -export const CONSUMABLE_PRODUCT_IDS: string[] = [ - 'dev.hyo.martie.10bulbs', - 'dev.hyo.martie.30bulbs', -]; - -export const NON_CONSUMABLE_PRODUCT_IDS: string[] = [ - 'dev.hyo.martie.certified', -]; - -export const PRODUCT_IDS: string[] = [ - ...CONSUMABLE_PRODUCT_IDS, - ...NON_CONSUMABLE_PRODUCT_IDS, -]; - -export const SUBSCRIPTION_PRODUCT_IDS: string[] = [ - 'dev.hyo.martie.premium', - 'dev.hyo.martie.premium_year', -]; -export const DEFAULT_SUBSCRIPTION_PRODUCT_ID = SUBSCRIPTION_PRODUCT_IDS[0]; diff --git a/libraries/react-native-iap/example-expo/contexts/DataModalContext.tsx b/libraries/react-native-iap/example-expo/contexts/DataModalContext.tsx deleted file mode 100644 index 4b699f17..00000000 --- a/libraries/react-native-iap/example-expo/contexts/DataModalContext.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import React, { - createContext, - useContext, - useState, - useCallback, - useRef, - useEffect, -} from 'react'; -import { - Modal, - View, - Text, - TouchableOpacity, - ScrollView, - Platform, - Alert, - StyleSheet, -} from 'react-native'; -import Clipboard from '@react-native-clipboard/clipboard'; - -interface DataModalContextType { - showData: (data: any, title?: string) => void; - hideModal: () => void; -} - -const DataModalContext = createContext( - undefined, -); - -export function DataModalProvider({children}: {children: React.ReactNode}) { - const [visible, setVisible] = useState(false); - const [data, setData] = useState(null); - const [title, setTitle] = useState('Data Details'); - const resetTimeoutRef = useRef | null>(null); - - const showData = useCallback((newData: any, newTitle?: string) => { - if (resetTimeoutRef.current) { - clearTimeout(resetTimeoutRef.current); - resetTimeoutRef.current = null; - } - setData(newData); - setTitle(newTitle || 'Data Details'); - setVisible(true); - }, []); - - const hideModal = useCallback(() => { - setVisible(false); - resetTimeoutRef.current = setTimeout(() => { - setData(null); - setTitle('Data Details'); - resetTimeoutRef.current = null; - }, 300); - }, []); - - useEffect(() => { - return () => { - if (resetTimeoutRef.current) { - clearTimeout(resetTimeoutRef.current); - } - }; - }, []); - - const handleCopy = useCallback(() => { - if (!data) return; - - // Remove sensitive fields - const {purchaseToken, ...safeData} = data; - const jsonString = JSON.stringify(safeData, null, 2); - - Clipboard.setString(jsonString); - Alert.alert('Copied', 'Data copied to clipboard'); - }, [data]); - - return ( - - {children} - - - - - {/* Header */} - - {title} - - - - - - {/* Content */} - - - {(() => { - if (!data) return ''; - const {purchaseToken, ...safeData} = data; - return JSON.stringify(safeData, null, 2); - })()} - - - - {/* Footer */} - - - 📋 Copy JSON - - - Close - - - - - - - ); -} - -export function useDataModal() { - const context = useContext(DataModalContext); - if (!context) { - throw new Error('useDataModal must be used within DataModalProvider'); - } - return context; -} - -const styles = StyleSheet.create({ - overlay: { - flex: 1, - backgroundColor: 'rgba(0,0,0,0.4)', - justifyContent: 'center', - alignItems: 'center', - }, - modalContainer: { - backgroundColor: '#fff', - borderRadius: 16, - width: '90%', - height: '75%', - overflow: 'hidden', - shadowColor: '#000', - shadowOffset: {width: 0, height: 4}, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 8, - }, - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - padding: 16, - borderBottomWidth: 1, - borderBottomColor: '#eee', - backgroundColor: '#f8f9fa', - }, - headerTitle: { - fontSize: 18, - fontWeight: '600', - color: '#333', - }, - closeButton: { - fontSize: 24, - color: '#666', - fontWeight: '300', - }, - content: { - flex: 1, - padding: 16, - }, - jsonText: { - fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace', - fontSize: 12, - color: '#333', - lineHeight: 18, - }, - footer: { - flexDirection: 'row', - gap: 12, - padding: 16, - borderTopWidth: 1, - borderTopColor: '#eee', - backgroundColor: '#f8f9fa', - }, - copyButton: { - flex: 1, - backgroundColor: '#007AFF', - paddingVertical: 14, - borderRadius: 12, - alignItems: 'center', - shadowColor: '#000', - shadowOffset: {width: 0, height: 2}, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, - }, - closeFooterButton: { - flex: 1, - backgroundColor: '#6c757d', - paddingVertical: 14, - borderRadius: 12, - alignItems: 'center', - shadowColor: '#000', - shadowOffset: {width: 0, height: 2}, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, - }, - buttonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, -}); diff --git a/libraries/react-native-iap/example-expo/eslint.config.js b/libraries/react-native-iap/example-expo/eslint.config.js deleted file mode 100644 index 8efb5703..00000000 --- a/libraries/react-native-iap/example-expo/eslint.config.js +++ /dev/null @@ -1,27 +0,0 @@ -// https://docs.expo.dev/guides/using-eslint/ -const {defineConfig} = require('eslint/config'); -const expoConfig = require('eslint-config-expo/flat'); - -module.exports = defineConfig([ - expoConfig, - { - ignores: ['dist/*'], - }, - { - rules: { - // Disable import resolution for react-native-iap since it's locally linked - 'import/no-unresolved': [ - 'error', - { - ignore: ['^react-native-iap$'], - }, - ], - }, - }, - { - files: ['app/*.tsx'], - rules: { - 'react/no-unescaped-entities': 'off', - }, - }, -]); diff --git a/libraries/react-native-iap/example-expo/hooks/useColorScheme.ts b/libraries/react-native-iap/example-expo/hooks/useColorScheme.ts deleted file mode 100644 index fac8a01c..00000000 --- a/libraries/react-native-iap/example-expo/hooks/useColorScheme.ts +++ /dev/null @@ -1 +0,0 @@ -export {useColorScheme} from 'react-native'; diff --git a/libraries/react-native-iap/example-expo/hooks/useColorScheme.web.ts b/libraries/react-native-iap/example-expo/hooks/useColorScheme.web.ts deleted file mode 100644 index 81b080d8..00000000 --- a/libraries/react-native-iap/example-expo/hooks/useColorScheme.web.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {useEffect, useState} from 'react'; -import {useColorScheme as useRNColorScheme} from 'react-native'; - -/** - * To support static rendering, this value needs to be re-calculated on the client side for web - */ -export function useColorScheme() { - const [hasHydrated, setHasHydrated] = useState(false); - - useEffect(() => { - setHasHydrated(true); - }, []); - - const colorScheme = useRNColorScheme(); - - if (hasHydrated) { - return colorScheme; - } - - return 'light'; -} diff --git a/libraries/react-native-iap/example-expo/hooks/useThemeColor.ts b/libraries/react-native-iap/example-expo/hooks/useThemeColor.ts deleted file mode 100644 index f008e589..00000000 --- a/libraries/react-native-iap/example-expo/hooks/useThemeColor.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Learn more about light and dark modes: - * https://docs.expo.dev/guides/color-schemes/ - */ - -import {Colors} from '@/constants/Colors'; -import {useColorScheme} from '@/hooks/useColorScheme'; - -export function useThemeColor( - props: {light?: string; dark?: string}, - colorName: keyof typeof Colors.light & keyof typeof Colors.dark, -) { - const theme = useColorScheme() ?? 'light'; - const colorFromProps = props[theme]; - - if (colorFromProps) { - return colorFromProps; - } else { - return Colors[theme][colorName]; - } -} diff --git a/libraries/react-native-iap/example-expo/hooks/useVerificationMethod.ts b/libraries/react-native-iap/example-expo/hooks/useVerificationMethod.ts deleted file mode 100644 index 9f604a2e..00000000 --- a/libraries/react-native-iap/example-expo/hooks/useVerificationMethod.ts +++ /dev/null @@ -1,99 +0,0 @@ -import {useState, useCallback, useRef, useEffect} from 'react'; -import {Platform, ActionSheetIOS, Alert} from 'react-native'; - -export type VerificationMethod = 'ignore' | 'local' | 'iapkit'; - -interface UseVerificationMethodReturn { - verificationMethod: VerificationMethod; - verificationMethodRef: React.MutableRefObject; - setVerificationMethod: React.Dispatch< - React.SetStateAction - >; - showVerificationMethodSelector: () => void; - getVerificationMethodLabel: () => string; -} - -/** - * Hook to manage verification method selection with platform-specific UI - */ -export function useVerificationMethod( - initialMethod: VerificationMethod = 'ignore', -): UseVerificationMethodReturn { - const [verificationMethod, setVerificationMethod] = - useState(initialMethod); - const verificationMethodRef = useRef(verificationMethod); - - // Keep ref in sync with state - useEffect(() => { - verificationMethodRef.current = verificationMethod; - }, [verificationMethod]); - - const getVerificationMethodLabel = useCallback((): string => { - switch (verificationMethod) { - case 'ignore': - return 'None'; - case 'local': - return 'Local'; - case 'iapkit': - return 'IAPKit'; - default: - return 'Unknown'; - } - }, [verificationMethod]); - - const showVerificationMethodSelector = useCallback(() => { - const options = [ - 'None (Skip)', - 'Local (Device)', - 'IAPKit (Server)', - 'Cancel', - ]; - const cancelButtonIndex = 3; - - if (Platform.OS === 'ios') { - ActionSheetIOS.showActionSheetWithOptions( - { - options, - cancelButtonIndex, - title: 'Select Verification Method', - message: 'Choose how to verify purchases after completion', - }, - (buttonIndex) => { - if (buttonIndex === 0) { - setVerificationMethod('ignore'); - } else if (buttonIndex === 1) { - setVerificationMethod('local'); - } else if (buttonIndex === 2) { - setVerificationMethod('iapkit'); - } - }, - ); - } else { - // For Android, use simple Alert with buttons - Alert.alert( - 'Select Verification Method', - 'Choose how to verify purchases after completion', - [ - {text: 'None (Skip)', onPress: () => setVerificationMethod('ignore')}, - { - text: 'Local (Device)', - onPress: () => setVerificationMethod('local'), - }, - { - text: 'IAPKit (Server)', - onPress: () => setVerificationMethod('iapkit'), - }, - {text: 'Cancel', style: 'cancel'}, - ], - ); - } - }, []); - - return { - verificationMethod, - verificationMethodRef, - setVerificationMethod, - showVerificationMethodSelector, - getVerificationMethodLabel, - }; -} diff --git a/libraries/react-native-iap/example-expo/package.json b/libraries/react-native-iap/example-expo/package.json deleted file mode 100644 index 139d965b..00000000 --- a/libraries/react-native-iap/example-expo/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "example-expo", - "main": "index.js", - "version": "1.0.0", - "scripts": { - "setup": "./scripts/expo-setup.sh", - "start": "expo start", - "reset-project": "node ./scripts/reset-project.js", - "android": "expo run:android", - "ios": "expo run:ios", - "web": "expo start --web", - "lint": "expo lint", - "typecheck": "tsc --noEmit", - "postinstall": "./scripts/expo-setup.sh && ./scripts/copy-screens.sh" - }, - "dependencies": { - "@expo/vector-icons": "^15.0.2", - "@preact/signals-react": "^3.3.0", - "@react-native-clipboard/clipboard": "^1.16.3", - "@react-navigation/bottom-tabs": "^7.3.10", - "@react-navigation/elements": "^2.3.8", - "@react-navigation/native": "^7.1.6", - "expo": "^54.0.12", - "expo-blur": "~15.0.7", - "expo-constants": "~18.0.9", - "expo-font": "~14.0.8", - "expo-haptics": "~15.0.7", - "expo-image": "~3.0.8", - "expo-linking": "~8.0.8", - "expo-router": "~6.0.10", - "expo-splash-screen": "~31.0.10", - "expo-status-bar": "~3.0.8", - "expo-symbols": "~1.0.7", - "expo-system-ui": "~6.0.7", - "expo-web-browser": "~15.0.8", - "react": "19.1.0", - "react-dom": "19.1.0", - "react-native": "0.81.4", - "react-native-gesture-handler": "~2.28.0", - "react-native-nitro-modules": "^0.35.0", - "react-native-reanimated": "~4.1.1", - "react-native-safe-area-context": "~5.6.0", - "react-native-screens": "~4.16.0", - "react-native-web": "^0.21.0", - "react-native-webview": "13.15.0" - }, - "devDependencies": { - "@babel/core": "^7.25.2", - "@types/react": "~19.1.10", - "eslint": "^9.25.0", - "eslint-config-expo": "~10.0.0", - "expo-build-properties": "~1.0.9", - "typescript": "~5.9.2" - }, - "private": true -} diff --git a/libraries/react-native-iap/example-expo/scripts/copy-screens.sh b/libraries/react-native-iap/example-expo/scripts/copy-screens.sh deleted file mode 100755 index a7883722..00000000 --- a/libraries/react-native-iap/example-expo/scripts/copy-screens.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/bin/bash - -echo "📋 Copying screen files from example to example-expo..." - -# Check if we're in the example-expo directory -if [ ! -d "app" ]; then - echo "❌ Error: app directory not found. Make sure you're in the example-expo directory." - exit 1 -fi - -# Check if example directory exists -if [ ! -d "../example/screens" ]; then - echo "❌ Error: ../example/screens directory not found." - exit 1 -fi - -# Function to add generation comment to copied file and update imports -add_generation_comment() { - local source_file=$1 - local target_file=$2 - local source_name=$(basename "$source_file") - - # Create temp file with comment and modified content - { - echo "// Generated from example/screens/$source_name" - echo "// This file is automatically copied during postinstall" - echo "// Do not edit directly - modify the source file instead" - echo "" - # Read the source file and replace imports - # example uses ../src/*, example-expo uses ../* (components/, utils/, hooks/ at root level) - sed \ - -e "s|from '\.\./src/components/Loading'|from '../components/Loading'|g" \ - -e "s|from '\.\./src/utils/constants'|from '../constants/products'|g" \ - -e "s|from '\.\./src/components/PurchaseDetails'|from '../components/PurchaseDetails'|g" \ - -e "s|from '\.\./src/components/PurchaseSummaryRow'|from '../components/PurchaseSummaryRow'|g" \ - -e "s|from '\.\./src/contexts/DataModalContext'|from '../contexts/DataModalContext'|g" \ - -e "s|from '\.\./src/utils/errorUtils'|from '../utils/errorUtils'|g" \ - -e "s|from '\.\./src/hooks/useVerificationMethod'|from '../hooks/useVerificationMethod'|g" \ - -e "s|from '\.\./src/components/AndroidOneTimeOfferDetails'|from '../components/AndroidOneTimeOfferDetails'|g" \ - -e "s|import {IAPKIT_API_KEY} from '@env';|// IAPKit API Key - Set this in your environment or replace with your actual key\nconst IAPKIT_API_KEY = process.env.EXPO_PUBLIC_IAPKIT_API_KEY \|\| '';|g" \ - "$source_file" - } > "$target_file" -} - -# Copy and rename screen files from example to example-expo -copied_files=0 - -if [ -f "../example/screens/AvailablePurchases.tsx" ]; then - add_generation_comment "../example/screens/AvailablePurchases.tsx" "app/available-purchases.tsx" - echo "✅ Copied AvailablePurchases.tsx → available-purchases.tsx" - ((copied_files++)) -fi - -if [ -f "../example/screens/OfferCode.tsx" ]; then - add_generation_comment "../example/screens/OfferCode.tsx" "app/offer-code.tsx" - echo "✅ Copied OfferCode.tsx → offer-code.tsx" - ((copied_files++)) -fi - -if [ -f "../example/screens/SubscriptionFlow.tsx" ]; then - add_generation_comment "../example/screens/SubscriptionFlow.tsx" "app/subscription-flow.tsx" - echo "✅ Copied SubscriptionFlow.tsx → subscription-flow.tsx" - ((copied_files++)) -fi - -if [ -f "../example/screens/PurchaseFlow.tsx" ]; then - add_generation_comment "../example/screens/PurchaseFlow.tsx" "app/purchase-flow.tsx" - echo "✅ Copied PurchaseFlow.tsx → purchase-flow.tsx" - ((copied_files++)) -fi - -if [ $copied_files -eq 0 ]; then - echo "⚠️ No screen files found to copy." -else - echo "✅ Successfully copied $copied_files screen files!" - - # Run prettier on copied files to ensure consistent formatting - if command -v npx >/dev/null 2>&1; then - echo "🎨 Running prettier on copied files..." - npx prettier --write "app/available-purchases.tsx" "app/offer-code.tsx" "app/subscription-flow.tsx" "app/purchase-flow.tsx" 2>/dev/null || true - echo "✅ Prettier formatting complete!" - fi -fi \ No newline at end of file diff --git a/libraries/react-native-iap/example-expo/scripts/expo-setup.sh b/libraries/react-native-iap/example-expo/scripts/expo-setup.sh deleted file mode 100755 index 338d3ec0..00000000 --- a/libraries/react-native-iap/example-expo/scripts/expo-setup.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash - -echo "📦 Setting up react-native-iap for development..." - -# Go to root directory -cd .. - -# Install root dependencies first -echo "🔧 Installing root dependencies with yarn..." -yarn install - -# Clean and create react-native-iap directory in node_modules -echo "📁 Preparing node_modules/react-native-iap directory..." -rm -rf example-expo/node_modules/react-native-iap -mkdir -p example-expo/node_modules/react-native-iap - -# Create symlinks for development (TypeScript sources and native code) -echo "🔗 Creating symlinks for development..." - -# Copy all necessary directories and files -echo "📁 Copying source directories..." -cp -r src example-expo/node_modules/react-native-iap/ -cp -r ios example-expo/node_modules/react-native-iap/ -cp -r android example-expo/node_modules/react-native-iap/ - -# Copy essential files (not symlink to avoid issues) -echo "📄 Copying configuration files..." -cp package.json example-expo/node_modules/react-native-iap/ -cp nitro.json example-expo/node_modules/react-native-iap/ -cp NitroIap.podspec example-expo/node_modules/react-native-iap/ -cp openiap-versions.json example-expo/node_modules/react-native-iap/ -cp react-native.config.js example-expo/node_modules/react-native-iap/ 2>/dev/null || true -cp README.md example-expo/node_modules/react-native-iap/ 2>/dev/null || true -cp tsconfig.json example-expo/node_modules/react-native-iap/ 2>/dev/null || true -# Don't copy app.plugin.js here - will copy after building plugin - -# Build the plugin -echo "🔨 Building plugin..." -yarn build:plugin - -# Build the library -echo "🔨 Building library..." -yarn prepare - -# Copy plugin directory after building -echo "📁 Copying plugin directory..." -if [ -d "plugin" ]; then - cp -r plugin example-expo/node_modules/react-native-iap/ - echo "✅ plugin/ directory copied" -fi - -# Copy app.plugin.js after plugin is built -cp app.plugin.js example-expo/node_modules/react-native-iap/ - -# Copy built files -echo "📂 Copying built files..." -if [ -d "lib" ]; then - cp -r lib example-expo/node_modules/react-native-iap/ - echo "✅ lib/ directory copied" -fi - -if [ -d "nitrogen" ]; then - cp -r nitrogen example-expo/node_modules/react-native-iap/ - echo "✅ nitrogen/ directory copied" -fi - -# Function to add generation comment to copied file -add_generation_comment() { - local source_file=$1 - local target_file=$2 - local source_name=$(basename "$source_file") - - # Create temp file with comment and original content - { - echo "// Generated from example/screens/$source_name" - echo "// This file is automatically copied during postinstall" - echo "// Do not edit directly - modify the source file instead" - echo "" - cat "$source_file" - } > "$target_file" -} - -# Return to example-expo directory -cd example-expo diff --git a/libraries/react-native-iap/example-expo/scripts/reset-project.js b/libraries/react-native-iap/example-expo/scripts/reset-project.js deleted file mode 100755 index a6dd1e2a..00000000 --- a/libraries/react-native-iap/example-expo/scripts/reset-project.js +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env node - -/** - * This script is used to reset the project to a blank state. - * It deletes or moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example based on user input and creates a new /app directory with an index.tsx and _layout.tsx file. - * You can remove the `reset-project` script from package.json and safely delete this file after running it. - */ - -const fs = require('fs'); -const path = require('path'); -const readline = require('readline'); - -const root = process.cwd(); -const oldDirs = ['app', 'components', 'hooks', 'constants', 'scripts']; -const exampleDir = 'app-example'; -const newAppDir = 'app'; -const exampleDirPath = path.join(root, exampleDir); - -const indexContent = `import { Text, View } from "react-native"; - -export default function Index() { - return ( - - Edit app/index.tsx to edit this screen. - - ); -} -`; - -const layoutContent = `import { Stack } from "expo-router"; - -export default function RootLayout() { - return ; -} -`; - -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, -}); - -const moveDirectories = async (userInput) => { - try { - if (userInput === 'y') { - // Create the app-example directory - await fs.promises.mkdir(exampleDirPath, {recursive: true}); - console.log(`📁 /${exampleDir} directory created.`); - } - - // Move old directories to new app-example directory or delete them - for (const dir of oldDirs) { - const oldDirPath = path.join(root, dir); - if (fs.existsSync(oldDirPath)) { - if (userInput === 'y') { - const newDirPath = path.join(root, exampleDir, dir); - await fs.promises.rename(oldDirPath, newDirPath); - console.log(`➡️ /${dir} moved to /${exampleDir}/${dir}.`); - } else { - await fs.promises.rm(oldDirPath, {recursive: true, force: true}); - console.log(`❌ /${dir} deleted.`); - } - } else { - console.log(`➡️ /${dir} does not exist, skipping.`); - } - } - - // Create new /app directory - const newAppDirPath = path.join(root, newAppDir); - await fs.promises.mkdir(newAppDirPath, {recursive: true}); - console.log('\n📁 New /app directory created.'); - - // Create index.tsx - const indexPath = path.join(newAppDirPath, 'index.tsx'); - await fs.promises.writeFile(indexPath, indexContent); - console.log('📄 app/index.tsx created.'); - - // Create _layout.tsx - const layoutPath = path.join(newAppDirPath, '_layout.tsx'); - await fs.promises.writeFile(layoutPath, layoutContent); - console.log('📄 app/_layout.tsx created.'); - - console.log('\n✅ Project reset complete. Next steps:'); - console.log( - `1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${ - userInput === 'y' - ? `\n3. Delete the /${exampleDir} directory when you're done referencing it.` - : '' - }`, - ); - } catch (error) { - console.error(`❌ Error during script execution: ${error.message}`); - } -}; - -rl.question( - 'Do you want to move existing files to /app-example instead of deleting them? (Y/n): ', - (answer) => { - const userInput = answer.trim().toLowerCase() || 'y'; - if (userInput === 'y' || userInput === 'n') { - moveDirectories(userInput).finally(() => rl.close()); - } else { - console.log("❌ Invalid input. Please enter 'Y' or 'N'."); - rl.close(); - } - }, -); diff --git a/libraries/react-native-iap/example-expo/tsconfig.json b/libraries/react-native-iap/example-expo/tsconfig.json deleted file mode 100644 index 3505e7a7..00000000 --- a/libraries/react-native-iap/example-expo/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "expo/tsconfig.base", - "compilerOptions": { - "strict": true, - "skipLibCheck": true, - "baseUrl": ".", - "paths": { - "@/*": ["./*"], - "react-native-iap": ["../src/index"] - } - }, - "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] -} diff --git a/libraries/react-native-iap/example-expo/utils/buildPurchaseRows.ts b/libraries/react-native-iap/example-expo/utils/buildPurchaseRows.ts deleted file mode 100644 index 0257afbe..00000000 --- a/libraries/react-native-iap/example-expo/utils/buildPurchaseRows.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type {Purchase} from 'react-native-iap'; - -export type PurchaseDetailRow = { - label: string; - value: string; -}; - -const formatDate = (timestamp?: number | null): string | undefined => { - if (typeof timestamp !== 'number' || !Number.isFinite(timestamp)) { - return undefined; - } - const date = new Date(timestamp); - if (Number.isNaN(date.getTime())) { - return undefined; - } - return date.toLocaleDateString(); -}; - -const pushRow = ( - rows: PurchaseDetailRow[], - label: string, - value?: string | number | null, -) => { - if (value === undefined || value === null || `${value}`.trim() === '') { - return; - } - rows.push({label, value: `${value}`}); -}; - -export const buildPurchaseRows = (purchase: Purchase): PurchaseDetailRow[] => { - const rows: PurchaseDetailRow[] = []; - - // Essential purchase information - pushRow(rows, 'Product ID', purchase.productId); - pushRow(rows, 'Transaction ID', purchase.id); - pushRow(rows, 'Platform', purchase.platform || 'Unknown'); - pushRow(rows, 'Purchase Date', formatDate(purchase.transactionDate)); - pushRow(rows, 'Quantity', purchase.quantity); - - if (purchase.purchaseState) { - const state = - purchase.purchaseState.charAt(0).toUpperCase() + - purchase.purchaseState.slice(1).toLowerCase(); - pushRow(rows, 'Status', state); - } - - // Platform-specific key information - const platform = (purchase.platform || '').toString().toLowerCase(); - - if (platform === 'ios') { - const iosPurchase = purchase as any; - pushRow(rows, 'App Account Token', iosPurchase.appAccountToken); - pushRow(rows, 'Expiration Date', formatDate(iosPurchase.expirationDateIOS)); - pushRow(rows, 'Auto Renewing', purchase.isAutoRenewing ? 'Yes' : 'No'); - } else if (platform === 'android') { - pushRow(rows, 'Auto Renewing', purchase.isAutoRenewing ? 'Yes' : 'No'); - } - - return rows; -}; diff --git a/libraries/react-native-iap/example-expo/utils/errorUtils.ts b/libraries/react-native-iap/example-expo/utils/errorUtils.ts deleted file mode 100644 index f0408307..00000000 --- a/libraries/react-native-iap/example-expo/utils/errorUtils.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Extract error message from various error formats - * Handles Error objects, objects with errors array, and other formats - */ -export function getErrorMessage(error: unknown): string { - if (error instanceof Error) { - return error.message; - } - - // Handle primitive types - preserve the original message - if ( - typeof error === 'string' || - typeof error === 'number' || - typeof error === 'boolean' - ) { - return String(error); - } - - if ( - error && - typeof error === 'object' && - 'errors' in error && - Array.isArray((error as {errors: unknown[]}).errors) - ) { - const errors = (error as {errors: {message?: string}[]}).errors; - if (errors.length === 0) { - return 'Unknown error'; - } - return errors[0]?.message || JSON.stringify(errors[0]) || 'Unknown error'; - } - - if (error && typeof error === 'object' && 'message' in error) { - return String((error as {message: unknown}).message); - } - - return String(error ?? 'Unknown error'); -} diff --git a/libraries/react-native-iap/example-expo/yarn.lock b/libraries/react-native-iap/example-expo/yarn.lock deleted file mode 100644 index e69de29b..00000000 diff --git a/libraries/react-native-iap/example/android/app/build.gradle b/libraries/react-native-iap/example/android/app/build.gradle index b938be93..a6b1b95e 100644 --- a/libraries/react-native-iap/example/android/app/build.gradle +++ b/libraries/react-native-iap/example/android/app/build.gradle @@ -73,17 +73,17 @@ def enableProguardInReleaseBuilds = false def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+' android { - ndkVersion rootProject.ext.ndkVersion - buildToolsVersion rootProject.ext.buildToolsVersion - compileSdk rootProject.ext.compileSdkVersion + ndkVersion = rootProject.ext.ndkVersion + buildToolsVersion = rootProject.ext.buildToolsVersion + compileSdk = rootProject.ext.compileSdkVersion - namespace "dev.hyo.martie" + namespace = "dev.hyo.martie" defaultConfig { - applicationId "dev.hyo.martie" - minSdkVersion rootProject.ext.minSdkVersion - targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 1 - versionName "1.0" + applicationId = "dev.hyo.martie" + minSdkVersion = rootProject.ext.minSdkVersion + targetSdkVersion = rootProject.ext.targetSdkVersion + versionCode = 1 + versionName = "1.0" } compileOptions { @@ -99,21 +99,21 @@ android { } signingConfigs { debug { - storeFile file('debug.keystore') - storePassword 'android' - keyAlias 'androiddebugkey' - keyPassword 'android' + storeFile = file('debug.keystore') + storePassword = 'android' + keyAlias = 'androiddebugkey' + keyPassword = 'android' } } buildTypes { debug { - signingConfig signingConfigs.debug + signingConfig = signingConfigs.debug } release { // Caution! In production, you need to generate your own keystore file. // see https://reactnative.dev/docs/signed-apk-android. - signingConfig signingConfigs.debug - minifyEnabled enableProguardInReleaseBuilds + signingConfig = signingConfigs.debug + minifyEnabled = enableProguardInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } } diff --git a/libraries/react-native-iap/example/screens/AllProducts.tsx b/libraries/react-native-iap/example/screens/AllProducts.tsx index 716eec26..e93d0a8e 100644 --- a/libraries/react-native-iap/example/screens/AllProducts.tsx +++ b/libraries/react-native-iap/example/screens/AllProducts.tsx @@ -77,8 +77,6 @@ function AllProducts() { useEffect(() => { console.log('[AllProducts] useEffect - connected:', connected); - console.log('[AllProducts] Current products:', products.length); - console.log('[AllProducts] Current subscriptions:', subscriptions.length); if (connected) { console.log( @@ -90,11 +88,6 @@ function AllProducts() { fetchProducts({skus: ALL_PRODUCT_IDS, type: 'all'}) .then(() => { console.log('[AllProducts] fetchProducts completed'); - console.log('[AllProducts] Products after fetch:', products.length); - console.log( - '[AllProducts] Subscriptions after fetch:', - subscriptions.length, - ); }) .catch((error) => { console.error('[AllProducts] fetchProducts error:', error); @@ -378,7 +371,7 @@ function AllProducts() { index: number, ) => ( diff --git a/libraries/react-native-iap/example/screens/AlternativeBilling.tsx b/libraries/react-native-iap/example/screens/AlternativeBilling.tsx index adde013b..3b1b1321 100644 --- a/libraries/react-native-iap/example/screens/AlternativeBilling.tsx +++ b/libraries/react-native-iap/example/screens/AlternativeBilling.tsx @@ -98,7 +98,11 @@ function AlternativeBillingScreen() { enableBillingProgramAndroid: Platform.OS === 'android' ? billingProgram : undefined, onPurchaseSuccess: async (purchase: Purchase) => { - console.log('Purchase successful:', purchase); + console.log('Purchase successful:', { + productId: purchase.productId, + transactionId: purchase.id, + platform: purchase.platform, + }); setLastPurchase(purchase); setIsProcessing(false); @@ -156,14 +160,14 @@ function AlternativeBillingScreen() { '[Android] User selected developer billing (External Payments)', ); console.log( - '[Android] External transaction token:', - details.externalTransactionToken, + '[Android] External transaction token available:', + Boolean(details.externalTransactionToken), ); setExternalPaymentsToken(details.externalTransactionToken); setIsProcessing(false); setPurchaseResult( - `✅ User selected Developer Billing (External Payments)\n\nToken: ${details.externalTransactionToken.substring(0, 30)}...\n\n⚠️ Important:\n1. Process payment through your external system\n2. Report token to Google Play within 24 hours`, + `✅ User selected Developer Billing (External Payments)\n\nToken: ${details.externalTransactionToken}\n\n⚠️ Important:\n1. Process payment through your external system\n2. Report token to Google Play within 24 hours`, ); Alert.alert( @@ -299,13 +303,11 @@ function AlternativeBillingScreen() { // Step 3: Create reporting token (after user completes external purchase) const details = await createBillingProgramReportingDetailsAndroid(billingProgram); - console.log( - '[Android] Reporting token created:', - details.externalTransactionToken.substring(0, 20) + '...', - ); + const hasReportingToken = Boolean(details.externalTransactionToken); + console.log('[Android] Reporting token created:', hasReportingToken); setPurchaseResult( - `✅ Billing Programs API completed\n\nProgram: ${billingProgram}\nURL: ${externalUrl}\nToken: ${details.externalTransactionToken.substring(0, 30)}...\n\n⚠️ Important:\n1. User completes purchase externally\n2. Report token to Google Play within 24h`, + `✅ Billing Programs API completed\n\nProgram: ${billingProgram}\nURL: ${externalUrl}\nToken: ${details.externalTransactionToken ?? 'missing'}\n\n⚠️ Important:\n1. User completes purchase externally\n2. Report token to Google Play within 24h`, ); Alert.alert( @@ -642,9 +644,7 @@ function AlternativeBillingScreen() { External Payments Token (Japan) - - Token: {externalPaymentsToken.substring(0, 40)}... - + Token: {externalPaymentsToken} ⚠️ Report this token to Google Play within 24 hours{'\n'} ℹ️ Process external payment through your system diff --git a/libraries/react-native-iap/example/screens/AvailablePurchases.tsx b/libraries/react-native-iap/example/screens/AvailablePurchases.tsx index 0520cf7d..6ea2eb3b 100644 --- a/libraries/react-native-iap/example/screens/AvailablePurchases.tsx +++ b/libraries/react-native-iap/example/screens/AvailablePurchases.tsx @@ -37,7 +37,11 @@ export default function AvailablePurchases() { finishTransaction, } = useIAP({ onPurchaseSuccess: async (purchase) => { - console.log('[AVAILABLE-PURCHASES] Purchase successful:', purchase); + console.log('[AVAILABLE-PURCHASES] Purchase successful:', { + productId: purchase.productId, + transactionId: purchase.id, + platform: purchase.platform, + }); // Finish transaction like in subscription-flow await finishTransaction({ @@ -69,7 +73,11 @@ export default function AvailablePurchases() { setIsCheckingStatus(true); try { const subs = await getActiveSubscriptions(); - console.log('[AVAILABLE-PURCHASES] Active subscriptions result:', subs); + console.log( + '[AVAILABLE-PURCHASES] Active subscriptions result:', + subs.length, + 'items', + ); } catch (error) { console.error( '[AVAILABLE-PURCHASES] Error checking subscription status:', @@ -149,7 +157,7 @@ export default function AvailablePurchases() { console.log( '[AVAILABLE-PURCHASES] activeSubscriptions:', activeSubscriptions.length, - activeSubscriptions, + 'items', ); }, [activeSubscriptions]); diff --git a/libraries/react-native-iap/example/screens/PurchaseFlow.tsx b/libraries/react-native-iap/example/screens/PurchaseFlow.tsx index 1d8fce2a..9f21568d 100644 --- a/libraries/react-native-iap/example/screens/PurchaseFlow.tsx +++ b/libraries/react-native-iap/example/screens/PurchaseFlow.tsx @@ -724,10 +724,10 @@ function PurchaseFlowContainer() { iapkit: { apiKey: '***hidden***', ...(Platform.OS === 'ios' - ? {apple: {jws: `${jwsOrToken.substring(0, 50)}...`}} + ? {apple: {jws: jwsOrToken}} : { google: { - purchaseToken: `${jwsOrToken.substring(0, 50)}...`, + purchaseToken: jwsOrToken, }, }), }, @@ -772,7 +772,7 @@ function PurchaseFlowContainer() { // ────────────────────────────────────────────────────────────────────── // Step 5: GRANT ENTITLEMENT // ────────────────────────────────────────────────────────────────────── - // TODO: In production, update your backend here: + // Production integration point: // - Save purchase record to database // - Unlock premium features for user // - Update user's subscription status diff --git a/libraries/react-native-iap/example/screens/SubscriptionFlow.tsx b/libraries/react-native-iap/example/screens/SubscriptionFlow.tsx index ea475cdf..7bfcfec2 100644 --- a/libraries/react-native-iap/example/screens/SubscriptionFlow.tsx +++ b/libraries/react-native-iap/example/screens/SubscriptionFlow.tsx @@ -24,7 +24,6 @@ import { type SubscriptionOffer, ErrorCode, } from 'react-native-iap'; -import {IAPKIT_API_KEY} from '@env'; import Loading from '../src/components/Loading'; import {SUBSCRIPTION_PRODUCT_IDS} from '../src/utils/constants'; import {getErrorMessage} from '../src/utils/errorUtils'; @@ -33,6 +32,7 @@ import { type VerificationMethod, } from '../src/hooks/useVerificationMethod'; import PurchaseSummaryRow from '../src/components/PurchaseSummaryRow'; +import {IAPKIT_API_KEY} from '@env'; type ExtendedPurchase = Purchase & { purchaseTokenAndroid?: string; @@ -418,13 +418,11 @@ function SubscriptionFlow({ targetBasePlanId, offerToken: targetOffer.offerTokenAndroid, replacementMode, - purchaseToken: tokenString - ? `<${tokenString.substring(0, 10)}...>` - : 'missing', + purchaseToken: tokenString || 'missing', allOffers: androidOffers?.map((o) => ({ basePlanId: o.basePlanIdAndroid, offerId: o.id, - offerToken: o.offerTokenAndroid?.substring(0, 20) + '...', + offerToken: o.offerTokenAndroid, })), }); @@ -445,8 +443,10 @@ function SubscriptionFlow({ }, type: 'subs', }).catch((err: PurchaseError) => { - console.error('Plan change failed:', err); - console.error('Full error:', JSON.stringify(err)); + console.error('Plan change failed:', { + code: err.code, + message: err.message, + }); // More helpful error messages let errorMessage = err.message; @@ -1651,8 +1651,7 @@ function SubscriptionFlowContainer() { // iOS: Check transactionState (purchased/pending/failed/deferred) // Android: Check purchaseState (0=pending, 1=purchased, 2=failed) onPurchaseSuccess: async (purchase: Purchase) => { - const {purchaseToken, ...safePurchase} = purchase || {}; - console.log('Purchase successful (redacted):', safePurchase); + console.log('Purchase successful:', purchase); // Try to detect which plan was purchased if (Platform.OS === 'ios') { @@ -1668,11 +1667,10 @@ function SubscriptionFlowContainer() { // Android: Check if we have offerToken or other data to identify the plan const purchaseData = purchase as ExtendedPurchase; - // Log full purchase data to understand what's available - console.log( - 'Full purchase data for plan detection:', - JSON.stringify(purchaseData, null, 2), - ); + console.log('Purchase data for plan detection:', { + productId: purchase.productId, + hasOfferToken: Boolean(purchaseData.offerToken), + }); // Map offerToken to basePlanId using fetched subscription data (cross-platform) if (purchaseData.offerToken) { @@ -1690,10 +1688,7 @@ function SubscriptionFlowContainer() { ); } else { // Fallback if we can't find the matching offer - console.log( - 'Could not map offerToken to basePlanId:', - purchaseData.offerToken, - ); + console.log('Could not map offerToken to basePlanId'); } } } @@ -1804,10 +1799,10 @@ function SubscriptionFlowContainer() { iapkit: { apiKey: '***hidden***', ...(Platform.OS === 'ios' - ? {apple: {jws: `${jwsOrToken.substring(0, 50)}...`}} + ? {apple: {jws: jwsOrToken}} : { google: { - purchaseToken: `${jwsOrToken.substring(0, 50)}...`, + purchaseToken: jwsOrToken, }, }), }, @@ -1842,7 +1837,10 @@ function SubscriptionFlowContainer() { } } } catch (error) { - console.warn('[SubscriptionFlow] Verification failed:', error); + console.warn( + '[SubscriptionFlow] Verification failed:', + getErrorMessage(error), + ); Alert.alert( 'Verification Failed', `Purchase verification failed: ${getErrorMessage(error)}`, @@ -1855,7 +1853,7 @@ function SubscriptionFlowContainer() { // ────────────────────────────────────────────────────────────────────── // STEP 4: GRANT ENTITLEMENT // ────────────────────────────────────────────────────────────────────── - // TODO: In production, update your backend here: + // Production integration point: // - Save subscription record to database // - Unlock premium features for user // - Update user's subscription status @@ -1909,7 +1907,7 @@ function SubscriptionFlowContainer() { try { await getActiveSubscriptions(SUBSCRIPTION_PRODUCT_IDS); } catch (e) { - console.warn('Failed to refresh subscriptions:', e); + console.warn('Failed to refresh subscriptions:', getErrorMessage(e)); } Alert.alert('Success', 'Purchase completed successfully!'); @@ -1919,7 +1917,10 @@ function SubscriptionFlowContainer() { // Purchase Error Handler // ──────────────────────────────────────────────────────────────────────── onPurchaseError: (error: PurchaseError) => { - console.error('Subscription failed:', error); + console.error('Subscription failed:', { + code: error.code, + message: error.message, + }); setIsProcessing(false); const dt = Date.now() - lastSuccessAtRef.current; if (error?.code === ErrorCode.ServiceError && dt >= 0 && dt < 1500) { @@ -2033,7 +2034,15 @@ function SubscriptionFlowContainer() { const activeSubs = await getActiveSubscriptions(); console.log('\n===== Active Subscriptions Check ====='); console.log('Total subscriptions:', activeSubs.length); - console.log('Full data:', JSON.stringify(activeSubs, null, 2)); + console.log( + 'Subscription summary:', + activeSubs.map((sub) => ({ + productId: sub.productId, + isActive: sub.isActive, + expirationDateIOS: sub.expirationDateIOS, + environmentIOS: sub.environmentIOS, + })), + ); // For iOS, check if there's a pending change in renewalInfo if (Platform.OS === 'ios') { @@ -2117,7 +2126,10 @@ function SubscriptionFlowContainer() { console.log('===================================\n'); } } catch (error) { - console.error('Error checking subscription status:', error); + console.error( + 'Error checking subscription status:', + getErrorMessage(error), + ); } finally { setIsCheckingStatus(false); } @@ -2176,7 +2188,10 @@ function SubscriptionFlowContainer() { }, type: 'subs', }).catch((err: PurchaseError) => { - console.warn('requestPurchase failed:', err); + console.warn('requestPurchase failed:', { + code: err.code, + message: err.message, + }); setIsProcessing(false); setPurchaseResult(`❌ Subscription failed: ${err.message}`); Alert.alert('Subscription Failed', err.message); @@ -2208,7 +2223,10 @@ function SubscriptionFlowContainer() { try { await deepLinkToSubscriptions(); } catch (error) { - console.warn('Failed to open subscription management:', error); + console.warn( + 'Failed to open subscription management:', + getErrorMessage(error), + ); Alert.alert( 'Cannot Open', 'Unable to open the subscription management screen on this device.', diff --git a/libraries/react-native-iap/example/screens/WebhookStream.tsx b/libraries/react-native-iap/example/screens/WebhookStream.tsx index c848e9c2..350a5c60 100644 --- a/libraries/react-native-iap/example/screens/WebhookStream.tsx +++ b/libraries/react-native-iap/example/screens/WebhookStream.tsx @@ -119,7 +119,7 @@ export default function WebhookStream() { IAPKit SSE + test notification api key:{' '} - {IAPKIT_API_KEY ? `${IAPKIT_API_KEY.slice(0, 8)}...` : 'MISSING'} + {IAPKIT_API_KEY ? 'CONFIGURED' : 'MISSING'} diff --git a/libraries/react-native-iap/example/src/components/AndroidOneTimeOfferDetails.tsx b/libraries/react-native-iap/example/src/components/AndroidOneTimeOfferDetails.tsx index d7161cdf..a3b39493 100644 --- a/libraries/react-native-iap/example/src/components/AndroidOneTimeOfferDetails.tsx +++ b/libraries/react-native-iap/example/src/components/AndroidOneTimeOfferDetails.tsx @@ -23,7 +23,10 @@ export default function AndroidOneTimeOfferDetails({ {offers.map( (offer: ProductAndroidOneTimePurchaseOfferDetail, index: number) => ( - + Offer {index + 1} {offer.offerId ? ` (${offer.offerId})` : ''} diff --git a/libraries/react-native-iap/example/src/contexts/DataModalContext.tsx b/libraries/react-native-iap/example/src/contexts/DataModalContext.tsx index 4b699f17..0cae11e7 100644 --- a/libraries/react-native-iap/example/src/contexts/DataModalContext.tsx +++ b/libraries/react-native-iap/example/src/contexts/DataModalContext.tsx @@ -63,9 +63,7 @@ export function DataModalProvider({children}: {children: React.ReactNode}) { const handleCopy = useCallback(() => { if (!data) return; - // Remove sensitive fields - const {purchaseToken, ...safeData} = data; - const jsonString = JSON.stringify(safeData, null, 2); + const jsonString = JSON.stringify(data, null, 2); Clipboard.setString(jsonString); Alert.alert('Copied', 'Data copied to clipboard'); @@ -96,8 +94,7 @@ export function DataModalProvider({children}: {children: React.ReactNode}) { {(() => { if (!data) return ''; - const {purchaseToken, ...safeData} = data; - return JSON.stringify(safeData, null, 2); + return JSON.stringify(data, null, 2); })()} diff --git a/libraries/react-native-iap/example/src/utils/buildPurchaseRows.ts b/libraries/react-native-iap/example/src/utils/buildPurchaseRows.ts index b574e1b3..fff3d575 100644 --- a/libraries/react-native-iap/example/src/utils/buildPurchaseRows.ts +++ b/libraries/react-native-iap/example/src/utils/buildPurchaseRows.ts @@ -89,7 +89,11 @@ export const buildPurchaseRows = (purchase: Purchase): PurchaseDetailRow[] => { if (platform === 'ios') { const iosPurchase = purchase as PurchaseIOS; pushRow(rows, 'quantityIOS', iosPurchase.quantityIOS); - pushRow(rows, 'appAccountToken', iosPurchase.appAccountToken); + pushRow( + rows, + 'appAccountToken', + iosPurchase.appAccountToken ?? null, + ); pushRow(rows, 'appBundleIdIOS', iosPurchase.appBundleIdIOS); pushRow(rows, 'countryCodeIOS', iosPurchase.countryCodeIOS); pushRow(rows, 'currencyCodeIOS', iosPurchase.currencyCodeIOS); @@ -134,7 +138,11 @@ export const buildPurchaseRows = (purchase: Purchase): PurchaseDetailRow[] => { } } else if (platform === 'android') { const androidPurchase = purchase as PurchaseAndroid; - pushRow(rows, 'signatureAndroid', androidPurchase.signatureAndroid); + pushRow( + rows, + 'signatureAndroid', + androidPurchase.signatureAndroid ?? null, + ); pushRow(rows, 'packageNameAndroid', androidPurchase.packageNameAndroid); pushRow( rows, @@ -161,10 +169,14 @@ export const buildPurchaseRows = (purchase: Purchase): PurchaseDetailRow[] => { 'autoRenewingAndroid', formatBoolean(androidPurchase.autoRenewingAndroid), ); - pushRow(rows, 'dataAndroid', androidPurchase.dataAndroid); + pushRow( + rows, + 'dataAndroid', + androidPurchase.dataAndroid ?? null, + ); } - pushRow(rows, 'purchaseToken', purchase.purchaseToken); + pushRow(rows, 'purchaseToken', purchase.purchaseToken ?? null); return rows; }; diff --git a/libraries/react-native-iap/ios/HybridRnIap.swift b/libraries/react-native-iap/ios/HybridRnIap.swift index 6822c5fa..10f9b270 100644 --- a/libraries/react-native-iap/ios/HybridRnIap.swift +++ b/libraries/react-native-iap/ios/HybridRnIap.swift @@ -388,7 +388,10 @@ class HybridRnIap: HybridRnIapSpec { RnIapLog.payload("validateReceiptIOS", ["sku": sku]) let props = try OpenIapSerialization.verifyPurchaseProps(from: ["apple": ["sku": sku]]) - let result = try await OpenIapModule.shared.validateReceiptIOS(props) + let verifyResult = try await OpenIapModule.shared.verifyPurchase(props) + guard case let .verifyPurchaseResultIos(result) = verifyResult else { + throw OpenIapException.make(code: .featureNotSupported, message: "Expected iOS validation result") + } var encoded = RnIapHelper.sanitizeDictionary(OpenIapSerialization.encode(result)) if encoded["receiptData"] != nil { encoded["receiptData"] = "" @@ -488,7 +491,7 @@ class HybridRnIap: HybridRnIapSpec { return Promise.async { do { RnIapLog.payload("getStorefront", nil) - let storefront = try await OpenIapModule.shared.getStorefrontIOS() + let storefront = try await OpenIapModule.shared.getStorefront() RnIapLog.result("getStorefront", storefront) return storefront } catch let purchaseError as PurchaseError { @@ -578,9 +581,12 @@ class HybridRnIap: HybridRnIapSpec { RnIapLog.payload("buyPromotedProductIOS", nil) let ok = try await OpenIapModule.shared.requestPurchaseOnPromotedProductIOS() RnIapLog.result("buyPromotedProductIOS", ok) + } catch let purchaseError as PurchaseError { + RnIapLog.failure("buyPromotedProductIOS", error: purchaseError) + throw OpenIapException.from(purchaseError) } catch { - // Event-only: OpenIAP will emit purchaseError for this flow. Avoid Promise rejection. RnIapLog.failure("buyPromotedProductIOS", error: error) + throw OpenIapException.make(code: .featureNotSupported, message: error.localizedDescription) } } } diff --git a/libraries/react-native-iap/nitro.json b/libraries/react-native-iap/nitro.json index 7d4694c3..c907ee10 100644 --- a/libraries/react-native-iap/nitro.json +++ b/libraries/react-native-iap/nitro.json @@ -15,7 +15,6 @@ }, "ignorePaths": [ "node_modules", - "example-expo", "example" ] } diff --git a/libraries/react-native-iap/package.json b/libraries/react-native-iap/package.json index 43d94376..727924e5 100644 --- a/libraries/react-native-iap/package.json +++ b/libraries/react-native-iap/package.json @@ -41,18 +41,16 @@ ], "scripts": { "build:plugin": "tsc --build plugin", - "setup:expo": "cd example-expo && bun setup", "example": "yarn workspace rn-iap-example", "example:ios": "yarn workspace rn-iap-example ios", "example:android": "yarn workspace rn-iap-example android", "example:start": "yarn workspace rn-iap-example start", - "typecheck": "yarn typecheck:lib && yarn typecheck:expo", + "typecheck": "yarn typecheck:lib", "typecheck:lib": "yarn tsc --noEmit", - "typecheck:expo": "cd example-expo && bun run typecheck", "clean": "rm -rf android/build node_modules/**/android/build lib nitrogen/generated", "lint": "eslint --ext .ts,.tsx,.js,.jsx src", "lint:eslint": "eslint --fix 'src/**/*.{ts,tsx}' 'plugin/src/**/*.{ts,tsx}'", - "lint:prettier": "prettier --write \"**/*.{md,js,jsx,ts,tsx}\" \"!example-expo/**\"", + "lint:prettier": "prettier --write \"**/*.{md,js,jsx,ts,tsx}\"", "lint:tsc": "tsc -p tsconfig.json --noEmit --skipLibCheck", "lint:ci": "yarn lint:tsc && yarn lint:eslint && yarn lint:prettier", "prepare": "npx tsx scripts/check-nitro-versions.ts && bob build && yarn nitrogen && yarn build:plugin", @@ -61,7 +59,7 @@ "test": "jest --coverage", "test:library": "jest --coverage", "test:example": "yarn workspace rn-iap-example test --coverage", - "test:plugin": "cd example-expo/plugin && bun test", + "test:plugin": "jest plugin --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", diff --git a/libraries/react-native-iap/scripts/ci-check.sh b/libraries/react-native-iap/scripts/ci-check.sh index fa2e85d6..de787f19 100755 --- a/libraries/react-native-iap/scripts/ci-check.sh +++ b/libraries/react-native-iap/scripts/ci-check.sh @@ -15,66 +15,31 @@ NC='\033[0m' # No Color # Track if any checks fail FAILED=0 -# 1. Install dependencies -echo -e "\n${YELLOW}📦 Installing dependencies...${NC}" -yarn install --immutable -if [ $? -ne 0 ]; then - echo -e "${RED}❌ Dependency installation failed${NC}" - FAILED=1 -else - echo -e "${GREEN}✅ Dependencies installed${NC}" -fi - -# 2. Generate Nitro code -echo -e "\n${YELLOW}⚙️ Generating Nitro code...${NC}" -yarn nitrogen -if [ $? -ne 0 ]; then - echo -e "${RED}❌ Nitro code generation failed${NC}" - FAILED=1 -else - echo -e "${GREEN}✅ Nitro code generated${NC}" -fi - -# 3. TypeScript check -echo -e "\n${YELLOW}🔍 Running TypeScript check...${NC}" -yarn typecheck -if [ $? -ne 0 ]; then - echo -e "${RED}❌ TypeScript check failed${NC}" - FAILED=1 -else - echo -e "${GREEN}✅ TypeScript check passed${NC}" -fi - -# 4. ESLint -echo -e "\n${YELLOW}🔍 Running ESLint...${NC}" -yarn lint -if [ $? -ne 0 ]; then - echo -e "${RED}❌ ESLint check failed${NC}" - FAILED=1 -else - echo -e "${GREEN}✅ ESLint check passed${NC}" -fi - -# 5. Prettier format check -echo -e "\n${YELLOW}💅 Checking code formatting...${NC}" -yarn prettier --check "src/**/*.{ts,tsx,js,jsx}" -if [ $? -ne 0 ]; then - echo -e "${RED}❌ Code formatting issues found${NC}" - echo -e "${YELLOW}💡 Run 'yarn prettier --write \"src/**/*.{ts,tsx,js,jsx}\"' to fix${NC}" - FAILED=1 -else - echo -e "${GREEN}✅ Code formatting check passed${NC}" -fi - -# 6. Run tests -echo -e "\n${YELLOW}🧪 Running tests...${NC}" -yarn test --passWithNoTests -if [ $? -ne 0 ]; then - echo -e "${RED}❌ Tests failed${NC}" - FAILED=1 -else - echo -e "${GREEN}✅ Tests passed${NC}" -fi +run_check() { + local title=$1 + local success_message=$2 + local failure_message=$3 + shift 3 + + echo -e "\n${YELLOW}${title}${NC}" + if "$@"; then + echo -e "${GREEN}✅ ${success_message}${NC}" + else + echo -e "${RED}❌ ${failure_message}${NC}" + if [ -n "${CHECK_HINT:-}" ]; then + echo -e "${YELLOW}💡 ${CHECK_HINT}${NC}" + fi + FAILED=1 + fi +} + +run_check "📦 Installing dependencies..." "Dependencies installed" "Dependency installation failed" yarn install --immutable +run_check "⚙️ Generating Nitro code..." "Nitro code generated" "Nitro code generation failed" yarn nitrogen +run_check "🔍 Running TypeScript check..." "TypeScript check passed" "TypeScript check failed" yarn typecheck +run_check "🔍 Running ESLint..." "ESLint check passed" "ESLint check failed" yarn lint +CHECK_HINT='Run '\''yarn prettier --write "src/**/*.{ts,tsx,js,jsx}"'\'' to fix' \ + run_check "💅 Checking code formatting..." "Code formatting check passed" "Code formatting issues found" yarn prettier --check "src/**/*.{ts,tsx,js,jsx}" +run_check "🧪 Running tests..." "Tests passed" "Tests failed" yarn test --passWithNoTests # Summary echo -e "\n================================" @@ -84,4 +49,4 @@ if [ $FAILED -eq 0 ]; then else echo -e "${RED}❌ Some CI checks failed. Please fix the issues before committing.${NC}" exit 1 -fi \ No newline at end of file +fi diff --git a/libraries/react-native-iap/src/__tests__/index.test.ts b/libraries/react-native-iap/src/__tests__/index.test.ts index d2348d3c..580e79b6 100644 --- a/libraries/react-native-iap/src/__tests__/index.test.ts +++ b/libraries/react-native-iap/src/__tests__/index.test.ts @@ -610,6 +610,16 @@ describe('Public API (src/index.ts)', () => { ).rejects.toThrow(/skus/); }); + it('throws on unsupported platform', async () => { + (Platform as any).OS = 'web'; + await expect( + IAP.requestPurchase({ + request: {ios: {sku: 'p1'}} as any, + type: 'in-app', + }), + ).rejects.toThrow(/Unsupported platform: web/); + }); + it('passes unified request to native', async () => { (Platform as any).OS = 'android'; await IAP.requestPurchase({ @@ -843,6 +853,35 @@ describe('Public API (src/index.ts)', () => { const passed = mockIap.requestPurchase.mock.calls.pop()?.[0]; expect(passed.ios.advancedCommerceData).toBe(advancedData); }); + + it('iOS subs forwards advanced subscription offer fields', async () => { + (Platform as any).OS = 'ios'; + await IAP.requestPurchase({ + request: { + apple: { + sku: 'premium_sub', + introductoryOfferEligibility: false, + promotionalOfferJWS: { + offerId: 'promo-offer', + jws: 'compact-jws', + }, + winBackOffer: { + offerId: 'winback-offer', + }, + }, + }, + type: 'subs', + }); + const passed = mockIap.requestPurchase.mock.calls.pop()?.[0]; + expect(passed.ios.introductoryOfferEligibility).toBe(false); + expect(passed.ios.promotionalOfferJWS).toEqual({ + offerId: 'promo-offer', + jws: 'compact-jws', + }); + expect(passed.ios.winBackOffer).toEqual({ + offerId: 'winback-offer', + }); + }); }); describe('getAvailablePurchases', () => { @@ -892,7 +931,7 @@ describe('Public API (src/index.ts)', () => { it('throws on unsupported platform', async () => { (Platform as any).OS = 'web'; await expect(IAP.getAvailablePurchases()).rejects.toThrow( - /Unsupported platform/, + /Unsupported platform: web/, ); }); }); @@ -940,6 +979,13 @@ describe('Public API (src/index.ts)', () => { IAP.finishTransaction({purchase: {id: 'tid'} as any}), ).resolves.toBeUndefined(); }); + + it('throws on unsupported platform', async () => { + (Platform as any).OS = 'web'; + await expect( + IAP.finishTransaction({purchase: {id: 'tid'} as any}), + ).rejects.toThrow(/Unsupported platform: web/); + }); }); describe('storefront helpers', () => { @@ -1377,6 +1423,23 @@ describe('Public API (src/index.ts)', () => { await expect(IAP.deepLinkToSubscriptions()).resolves.toBeUndefined(); expect(mockIap.showManageSubscriptionsIOS).toHaveBeenCalled(); }); + + it('deepLinkToSubscriptions surfaces iOS native failures', async () => { + (Platform as any).OS = 'ios'; + mockIap.deepLinkToSubscriptionsIOS = jest.fn(async () => { + throw new Error('scene missing'); + }); + await expect(IAP.deepLinkToSubscriptions()).rejects.toThrow( + 'scene missing', + ); + }); + + it('deepLinkToSubscriptions throws on unsupported platform', async () => { + (Platform as any).OS = 'web'; + await expect(IAP.deepLinkToSubscriptions()).rejects.toThrow( + 'Unsupported platform: web', + ); + }); }); describe('subscription helpers', () => { diff --git a/libraries/react-native-iap/src/hooks/useIAP.ts b/libraries/react-native-iap/src/hooks/useIAP.ts index 227b7c18..e80234b0 100644 --- a/libraries/react-native-iap/src/hooks/useIAP.ts +++ b/libraries/react-native-iap/src/hooks/useIAP.ts @@ -87,7 +87,7 @@ type UseIap = { * @remarks **Critical:** Android purchases must be finalized within 3 days or Google * auto-refunds. iOS unfinished transactions replay on every app launch. * - * @see {@link https://www.openiap.dev/docs/apis/finish-transaction} + * @see {@link https://openiap.dev/docs/apis/finish-transaction} */ finishTransaction: (args: MutationFinishTransactionArgs) => Promise; /** @@ -118,7 +118,7 @@ type UseIap = { * }, [availablePurchases, finishTransaction]); * ``` * - * @see {@link https://www.openiap.dev/docs/apis/get-available-purchases} + * @see {@link https://openiap.dev/docs/apis/get-available-purchases} */ getAvailablePurchases: (options?: PurchaseOptions) => Promise; /** @@ -148,7 +148,7 @@ type UseIap = { * @remarks This is a regular promise-based call. Don't confuse with `request*` APIs * (`requestPurchase`), which are event-based. * - * @see {@link https://www.openiap.dev/docs/apis/fetch-products} + * @see {@link https://openiap.dev/docs/apis/fetch-products} */ fetchProducts: (params: { skus: string[]; @@ -179,13 +179,13 @@ type UseIap = { * @remarks Event-based. Listen for the result via {@link purchaseUpdatedListener} / * {@link purchaseErrorListener}, or use `useIAP({ onPurchaseSuccess, onPurchaseError })`. * - * @see {@link https://www.openiap.dev/docs/apis/request-purchase} + * @see {@link https://openiap.dev/docs/apis/request-purchase} */ requestPurchase: (params: RequestPurchaseProps) => Promise; /** * @deprecated Use `verifyPurchase` instead. This function will be removed in a future version. * - * @see {@link https://www.openiap.dev/docs/apis/validate-receipt} + * @see {@link https://openiap.dev/docs/apis/validate-receipt} */ validateReceipt: ( options: VerifyPurchaseProps, @@ -193,7 +193,7 @@ type UseIap = { /** * Verify a purchase against your own backend (returns isValid + raw store metadata). * - * @see {@link https://www.openiap.dev/docs/features/validation#verify-purchase} + * @see {@link https://openiap.dev/docs/features/validation#verify-purchase} */ verifyPurchase: ( options: VerifyPurchaseProps, @@ -201,7 +201,7 @@ type UseIap = { /** * Verify via a managed provider — currently only `iapkit` (IAPKit). The PurchaseVerificationProvider enum exposes no other provider literal today. * - * @see {@link https://www.openiap.dev/docs/features/validation#verify-purchase-with-provider} + * @see {@link https://openiap.dev/docs/features/validation#verify-purchase-with-provider} */ verifyPurchaseWithProvider: ( options: VerifyPurchaseWithProviderProps, @@ -209,25 +209,25 @@ type UseIap = { /** * Restore non-consumable and active subscription purchases. * - * @see {@link https://www.openiap.dev/docs/apis/restore-purchases} + * @see {@link https://openiap.dev/docs/apis/restore-purchases} */ restorePurchases: (options?: PurchaseOptions) => Promise; /** * Read the App Store-promoted product, if any. * - * @see {@link https://www.openiap.dev/docs/apis/ios/get-promoted-product-ios} + * @see {@link https://openiap.dev/docs/apis/ios/get-promoted-product-ios} */ getPromotedProductIOS: () => Promise; /** * Buy the currently promoted product. * - * @see {@link https://www.openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios} + * @see {@link https://openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios} */ requestPurchaseOnPromotedProductIOS: () => Promise; /** * Get details of all currently active subscriptions. * - * @see {@link https://www.openiap.dev/docs/apis/get-active-subscriptions} + * @see {@link https://openiap.dev/docs/apis/get-active-subscriptions} */ getActiveSubscriptions: ( subscriptionIds?: string[], @@ -235,7 +235,7 @@ type UseIap = { /** * Check whether the user has any active subscription. * - * @see {@link https://www.openiap.dev/docs/apis/has-active-subscriptions} + * @see {@link https://openiap.dev/docs/apis/has-active-subscriptions} */ hasActiveSubscriptions: (subscriptionIds?: string[]) => Promise; /** @@ -248,19 +248,19 @@ type UseIap = { /** * Check whether alternative billing is available for the user. * - * @see {@link https://www.openiap.dev/docs/apis/android/check-alternative-billing-availability-android} + * @see {@link https://openiap.dev/docs/apis/android/check-alternative-billing-availability-android} */ checkAlternativeBillingAvailabilityAndroid?: () => Promise; /** * Display Google's alternative billing information dialog. * - * @see {@link https://www.openiap.dev/docs/apis/android/show-alternative-billing-dialog-android} + * @see {@link https://openiap.dev/docs/apis/android/show-alternative-billing-dialog-android} */ showAlternativeBillingDialogAndroid?: () => Promise; /** * Create a reporting token for an alternative billing flow. * - * @see {@link https://www.openiap.dev/docs/apis/android/create-alternative-billing-token-android} + * @see {@link https://openiap.dev/docs/apis/android/create-alternative-billing-token-android} */ createAlternativeBillingTokenAndroid?: ( sku?: string, diff --git a/libraries/react-native-iap/src/index.ts b/libraries/react-native-iap/src/index.ts index 0b2d5850..0f65e6fd 100644 --- a/libraries/react-native-iap/src/index.ts +++ b/libraries/react-native-iap/src/index.ts @@ -44,6 +44,8 @@ import type { RequestSubscriptionIosProps, RequestSubscriptionPropsByPlatforms, ActiveSubscription, + DeveloperProvidedBillingDetailsAndroid, + UserChoiceBillingDetails, } from './types'; import { convertNitroProductToProduct, @@ -72,8 +74,6 @@ import {parseAppTransactionPayload} from './utils'; // Import them here for use in this file's interfaces and functions. import type { BillingProgramAndroid, - ExternalLinkLaunchModeAndroid, - ExternalLinkTypeAndroid, } from './types'; // Export all types @@ -89,7 +89,7 @@ export * from './utils/error'; export type ProductTypeInput = 'inapp' | 'in-app' | 'subs'; const LEGACY_INAPP_WARNING = - "[react-native-iap] `type: 'inapp'` is deprecated and will be removed in v14.4.0. Use 'in-app' instead."; + "[react-native-iap] `type: 'inapp'` is deprecated and will be removed in a future major version. Use 'in-app' instead."; type NitroPurchaseRequest = Parameters[0]; type NitroAvailablePurchasesOptions = NonNullable< @@ -121,6 +121,9 @@ const toErrorMessage = (error: unknown): string => { return String(error ?? ''); }; +const unsupportedPlatformError = (): Error => + new Error(`Unsupported platform: ${Platform.OS}`); + export interface EventSubscription { remove(): void; } @@ -517,7 +520,7 @@ export const promotedProductListenerIOS = ( * const subscription = userChoiceBillingListenerAndroid((details) => { * console.log('User chose alternative billing'); * console.log('Products:', details.products); - * console.log('Token:', details.externalTransactionToken); + * console.log('External transaction token received; send it to your backend without logging it.'); * * // Send token to backend for Google Play reporting * await reportToGooglePlay(details.externalTransactionToken); @@ -531,7 +534,9 @@ type NitroUserChoiceBillingListener = Parameters< RnIap['addUserChoiceBillingListenerAndroid'] >[0]; -const userChoiceBillingJsListeners = new Set<(details: any) => void>(); +const userChoiceBillingJsListeners = new Set< + (details: UserChoiceBillingDetails) => void +>(); let userChoiceBillingNativeAttached = false; const userChoiceBillingNativeHandler: NitroUserChoiceBillingListener = ( details, @@ -549,7 +554,7 @@ const userChoiceBillingNativeHandler: NitroUserChoiceBillingListener = ( }; export const userChoiceBillingListenerAndroid = ( - listener: (details: any) => void, + listener: (details: UserChoiceBillingDetails) => void, ): EventSubscription => { if (Platform.OS !== 'android') { RnIapConsole.warn( @@ -602,7 +607,7 @@ export const userChoiceBillingListenerAndroid = ( * ```typescript * const subscription = developerProvidedBillingListenerAndroid((details) => { * console.log('User chose developer billing'); - * console.log('Token:', details.externalTransactionToken); + * console.log('External transaction token received; send it to your backend without logging it.'); * * // Process payment through your external payment system * await processExternalPayment(); @@ -637,15 +642,6 @@ const developerProvidedBillingNativeHandler: NitroDeveloperProvidedBillingListen } }; -export interface DeveloperProvidedBillingDetailsAndroid { - /** - * External transaction token used to report transactions made through developer billing. - * This token must be used when reporting the external transaction to Google Play. - * Must be reported within 24 hours of the transaction. - */ - externalTransactionToken: string; -} - export const developerProvidedBillingListenerAndroid = ( listener: (details: DeveloperProvidedBillingDetailsAndroid) => void, ): EventSubscription => { @@ -801,7 +797,7 @@ export const subscriptionBillingIssueListener = ( * @remarks This is a regular promise-based call. Don't confuse with `request*` APIs * (`requestPurchase`), which are event-based. * - * @see {@link https://www.openiap.dev/docs/apis/fetch-products} + * @see {@link https://openiap.dev/docs/apis/fetch-products} */ export const fetchProducts: QueryField<'fetchProducts'> = async (request) => { const {skus, type} = request; @@ -943,7 +939,7 @@ export const fetchProducts: QueryField<'fetchProducts'> = async (request) => { * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/get-available-purchases} + * @see {@link https://openiap.dev/docs/apis/get-available-purchases} */ export const getAvailablePurchases: QueryField< 'getAvailablePurchases' @@ -998,7 +994,7 @@ export const getAvailablePurchases: QueryField< return validPurchases.map(convertNitroPurchaseToPurchase); } else { - throw new Error('Unsupported platform'); + throw unsupportedPlatformError(); } } catch (error) { RnIapConsole.error('Failed to get available purchases:', error); @@ -1011,7 +1007,7 @@ export const getAvailablePurchases: QueryField< * @returns Promise - The promoted product or null if none available * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/get-promoted-product-ios} + * @see {@link https://openiap.dev/docs/apis/ios/get-promoted-product-ios} */ export const getPromotedProductIOS: QueryField< 'getPromotedProductIOS' @@ -1055,7 +1051,7 @@ export const requestPromotedProductIOS = getPromotedProductIOS; * console.log('User storefront:', storefront); // e.g., 'USA', 'GBR', 'KOR' * ``` * - * @see {@link https://www.openiap.dev/docs/apis/ios/get-storefront-ios} + * @see {@link https://openiap.dev/docs/apis/ios/get-storefront-ios} */ export const getStorefrontIOS: QueryField<'getStorefrontIOS'> = async () => { if (Platform.OS !== 'ios') { @@ -1074,7 +1070,7 @@ export const getStorefrontIOS: QueryField<'getStorefrontIOS'> = async () => { /** * Return the user's storefront country code. * - * @see {@link https://www.openiap.dev/docs/apis/get-storefront} + * @see {@link https://openiap.dev/docs/apis/get-storefront} */ export const getStorefront: QueryField<'getStorefront'> = async () => { if (Platform.OS !== 'ios' && Platform.OS !== 'android') { @@ -1128,7 +1124,7 @@ export const getStorefront: QueryField<'getStorefront'> = async () => { * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/ios/get-app-transaction-ios} + * @see {@link https://openiap.dev/docs/apis/ios/get-app-transaction-ios} */ export const getAppTransactionIOS: QueryField< 'getAppTransactionIOS' @@ -1169,7 +1165,7 @@ export const getAppTransactionIOS: QueryField< * @throws Error when called on non-iOS platforms or when IAP is not initialized * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/subscription-status-ios} + * @see {@link https://openiap.dev/docs/apis/ios/subscription-status-ios} */ export const subscriptionStatusIOS: QueryField< 'subscriptionStatusIOS' @@ -1202,7 +1198,7 @@ export const subscriptionStatusIOS: QueryField< * @returns Promise - Current entitlement or null * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/current-entitlement-ios} + * @see {@link https://openiap.dev/docs/apis/ios/current-entitlement-ios} */ export const currentEntitlementIOS: QueryField< 'currentEntitlementIOS' @@ -1236,7 +1232,7 @@ export const currentEntitlementIOS: QueryField< * @returns Promise - Latest transaction or null * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/latest-transaction-ios} + * @see {@link https://openiap.dev/docs/apis/ios/latest-transaction-ios} */ export const latestTransactionIOS: QueryField<'latestTransactionIOS'> = async ( sku, @@ -1269,7 +1265,7 @@ export const latestTransactionIOS: QueryField<'latestTransactionIOS'> = async ( * @returns Promise - Array of pending transactions * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/get-pending-transactions-ios} + * @see {@link https://openiap.dev/docs/apis/ios/get-pending-transactions-ios} */ export const getPendingTransactionsIOS: QueryField< 'getPendingTransactionsIOS' @@ -1300,7 +1296,7 @@ export const getPendingTransactionsIOS: QueryField< /** * List every StoreKit transaction (finished + unfinished) for the current user. * - * @see {@link https://www.openiap.dev/docs/apis/ios/get-all-transactions-ios} + * @see {@link https://openiap.dev/docs/apis/ios/get-all-transactions-ios} */ export const getAllTransactionsIOS: QueryField< 'getAllTransactionsIOS' @@ -1333,7 +1329,7 @@ export const getAllTransactionsIOS: QueryField< * @returns Promise - Subscriptions where auto-renewal status changed * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/show-manage-subscriptions-ios} + * @see {@link https://openiap.dev/docs/apis/ios/show-manage-subscriptions-ios} */ export const showManageSubscriptionsIOS: MutationField< 'showManageSubscriptionsIOS' @@ -1367,7 +1363,7 @@ export const showManageSubscriptionsIOS: MutationField< * @returns Promise - Eligibility status * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios} + * @see {@link https://openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios} */ export const isEligibleForIntroOfferIOS: QueryField< 'isEligibleForIntroOfferIOS' @@ -1395,7 +1391,7 @@ export const isEligibleForIntroOfferIOS: QueryField< * @returns Promise - Base64 encoded receipt data * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/get-receipt-data-ios} + * @see {@link https://openiap.dev/docs/apis/ios/get-receipt-data-ios} */ export const getReceiptDataIOS: QueryField<'getReceiptDataIOS'> = async () => { if (Platform.OS !== 'ios') { @@ -1484,7 +1480,7 @@ export const requestReceiptRefreshIOS = async (): Promise => { * @returns Promise - Verification status * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/is-transaction-verified-ios} + * @see {@link https://openiap.dev/docs/apis/ios/is-transaction-verified-ios} */ export const isTransactionVerifiedIOS: QueryField< 'isTransactionVerifiedIOS' @@ -1513,7 +1509,7 @@ export const isTransactionVerifiedIOS: QueryField< * @returns Promise - JWS representation or null * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/get-transaction-jws-ios} + * @see {@link https://openiap.dev/docs/apis/ios/get-transaction-jws-ios} */ export const getTransactionJwsIOS: QueryField<'getTransactionJwsIOS'> = async ( sku, @@ -1557,7 +1553,7 @@ export const getTransactionJwsIOS: QueryField<'getTransactionJwsIOS'> = async ( * @remarks When using `useIAP()`, connection is auto-managed on mount/unmount — * pass options to the hook instead of calling this directly. * - * @see {@link https://www.openiap.dev/docs/apis/init-connection} + * @see {@link https://openiap.dev/docs/apis/init-connection} */ export const initConnection: MutationField<'initConnection'> = async ( config, @@ -1581,7 +1577,7 @@ export const initConnection: MutationField<'initConnection'> = async ( /** * Close the store connection and release resources. * - * @see {@link https://www.openiap.dev/docs/apis/end-connection} + * @see {@link https://openiap.dev/docs/apis/end-connection} */ export const endConnection: MutationField<'endConnection'> = async () => { try { @@ -1604,7 +1600,7 @@ export const endConnection: MutationField<'endConnection'> = async () => { /** * Restore non-consumable and active subscription purchases. * - * @see {@link https://www.openiap.dev/docs/apis/restore-purchases} + * @see {@link https://openiap.dev/docs/apis/restore-purchases} */ export const restorePurchases: MutationField<'restorePurchases'> = async () => { try { @@ -1652,7 +1648,7 @@ export const restorePurchases: MutationField<'restorePurchases'> = async () => { * @remarks Event-based. Listen for the result via {@link purchaseUpdatedListener} / * {@link purchaseErrorListener}, or use `useIAP({ onPurchaseSuccess, onPurchaseError })`. * - * @see {@link https://www.openiap.dev/docs/apis/request-purchase} + * @see {@link https://openiap.dev/docs/apis/request-purchase} */ export const requestPurchase: MutationField<'requestPurchase'> = async ( request, @@ -1688,7 +1684,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( ); } } else { - throw new Error('Unsupported platform'); + throw unsupportedPlatformError(); } const unifiedRequest: NitroPurchaseRequest = {}; @@ -1723,6 +1719,22 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( if (iosRequest.advancedCommerceData) { iosPayload.advancedCommerceData = iosRequest.advancedCommerceData; } + if (isSubs) { + const subscriptionRequest = iosRequest as RequestSubscriptionIosProps; + if ( + subscriptionRequest.introductoryOfferEligibility !== undefined + ) { + iosPayload.introductoryOfferEligibility = + subscriptionRequest.introductoryOfferEligibility; + } + if (subscriptionRequest.promotionalOfferJWS) { + iosPayload.promotionalOfferJWS = + subscriptionRequest.promotionalOfferJWS; + } + if (subscriptionRequest.winBackOffer) { + iosPayload.winBackOffer = subscriptionRequest.winBackOffer; + } + } unifiedRequest.ios = iosPayload; } @@ -1820,7 +1832,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( * @remarks **Critical:** Android purchases must be finalized within 3 days or Google * auto-refunds. iOS unfinished transactions replay on every app launch. * - * @see {@link https://www.openiap.dev/docs/apis/finish-transaction} + * @see {@link https://openiap.dev/docs/apis/finish-transaction} */ export const finishTransaction: MutationField<'finishTransaction'> = async ( args, @@ -1851,7 +1863,7 @@ export const finishTransaction: MutationField<'finishTransaction'> = async ( }, }; } else { - throw new Error('Unsupported platform'); + throw unsupportedPlatformError(); } const result = await IAP.instance.finishTransaction(params); @@ -1889,7 +1901,7 @@ export const finishTransaction: MutationField<'finishTransaction'> = async ( * await acknowledgePurchaseAndroid('purchase_token_here'); * ``` * - * @see {@link https://www.openiap.dev/docs/apis/android/acknowledge-purchase-android} + * @see {@link https://openiap.dev/docs/apis/android/acknowledge-purchase-android} */ export const acknowledgePurchaseAndroid: MutationField< 'acknowledgePurchaseAndroid' @@ -1930,7 +1942,7 @@ export const acknowledgePurchaseAndroid: MutationField< * await consumePurchaseAndroid('purchase_token_here'); * ``` * - * @see {@link https://www.openiap.dev/docs/apis/android/consume-purchase-android} + * @see {@link https://openiap.dev/docs/apis/android/consume-purchase-android} */ export const consumePurchaseAndroid: MutationField< 'consumePurchaseAndroid' @@ -1987,7 +1999,7 @@ export const consumePurchaseAndroid: MutationField< * }); * ``` * - * @see {@link https://www.openiap.dev/docs/apis/validate-receipt} + * @see {@link https://openiap.dev/docs/apis/validate-receipt} */ export const validateReceipt: MutationField<'validateReceipt'> = async ( options, @@ -2118,7 +2130,7 @@ export const validateReceipt: MutationField<'validateReceipt'> = async ( * @param options - Receipt validation options containing the SKU * @returns Promise resolving to receipt validation result * - * @see {@link https://www.openiap.dev/docs/features/validation#verify-purchase} + * @see {@link https://openiap.dev/docs/features/validation#verify-purchase} */ export const verifyPurchase: MutationField<'verifyPurchase'> = validateReceipt; @@ -2129,7 +2141,7 @@ export const verifyPurchase: MutationField<'verifyPurchase'> = validateReceipt; * consumers who imported `validateReceiptIOS` — which is still declared on the * OpenIAP Query interface — keep working. Throws on non-iOS platforms. * - * @see {@link https://www.openiap.dev/docs/apis/ios/validate-receipt-ios} + * @see {@link https://openiap.dev/docs/apis/ios/validate-receipt-ios} */ export const validateReceiptIOS: QueryField<'validateReceiptIOS'> = async ( options, @@ -2162,7 +2174,7 @@ export const validateReceiptIOS: QueryField<'validateReceiptIOS'> = async ( * }); * ``` * - * @see {@link https://www.openiap.dev/docs/features/validation#verify-purchase-with-provider} + * @see {@link https://openiap.dev/docs/features/validation#verify-purchase-with-provider} */ export const verifyPurchaseWithProvider: MutationField< 'verifyPurchaseWithProvider' @@ -2207,7 +2219,7 @@ export const verifyPurchaseWithProvider: MutationField< * @returns Promise * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/sync-ios} + * @see {@link https://openiap.dev/docs/apis/ios/sync-ios} */ export const syncIOS: MutationField<'syncIOS'> = async () => { if (Platform.OS !== 'ios') { @@ -2234,7 +2246,7 @@ export const syncIOS: MutationField<'syncIOS'> = async () => { * @returns Promise - Indicates whether the redemption sheet was presented * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios} + * @see {@link https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios} */ export const presentCodeRedemptionSheetIOS: MutationField< 'presentCodeRedemptionSheetIOS' @@ -2278,7 +2290,7 @@ export const presentCodeRedemptionSheetIOS: MutationField< * @returns Promise - true when the request triggers successfully * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios} + * @see {@link https://openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios} */ export const requestPurchaseOnPromotedProductIOS = async (): Promise => { @@ -2322,7 +2334,7 @@ export const requestPurchaseOnPromotedProductIOS = * @returns Promise * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/clear-transaction-ios} + * @see {@link https://openiap.dev/docs/apis/ios/clear-transaction-ios} */ export const clearTransactionIOS: MutationField< 'clearTransactionIOS' @@ -2352,7 +2364,7 @@ export const clearTransactionIOS: MutationField< * @returns Promise - The refund status or null if not available * @platform iOS * - * @see {@link https://www.openiap.dev/docs/apis/ios/begin-refund-request-ios} + * @see {@link https://openiap.dev/docs/apis/ios/begin-refund-request-ios} */ export const beginRefundRequestIOS: MutationField< 'beginRefundRequestIOS' @@ -2380,7 +2392,7 @@ export const beginRefundRequestIOS: MutationField< * Deeplinks to native interface that allows users to manage their subscriptions * Cross-platform alias aligning with expo-iap * - * @see {@link https://www.openiap.dev/docs/apis/deep-link-to-subscriptions} + * @see {@link https://openiap.dev/docs/apis/deep-link-to-subscriptions} */ export const deepLinkToSubscriptions: MutationField< 'deepLinkToSubscriptions' @@ -2395,16 +2407,15 @@ export const deepLinkToSubscriptions: MutationField< return; } if (Platform.OS === 'ios') { - try { - if (typeof IAP.instance.deepLinkToSubscriptionsIOS === 'function') { - await IAP.instance.deepLinkToSubscriptionsIOS(); - } else { - await IAP.instance.showManageSubscriptionsIOS(); - } - } catch (error) { - RnIapConsole.warn('[deepLinkToSubscriptions] Failed on iOS:', error); + if (typeof IAP.instance.deepLinkToSubscriptionsIOS === 'function') { + await IAP.instance.deepLinkToSubscriptionsIOS(); + } else { + await IAP.instance.showManageSubscriptionsIOS(); } + return; } + + throw unsupportedPlatformError(); }; export const deepLinkToSubscriptionsIOS = async (): Promise => { @@ -2443,7 +2454,7 @@ export const deepLinkToSubscriptionsIOS = async (): Promise => { * @param subscriptionIds - Optional array of subscription IDs to filter by * @returns Promise - Array of active subscriptions * - * @see {@link https://www.openiap.dev/docs/apis/get-active-subscriptions} + * @see {@link https://openiap.dev/docs/apis/get-active-subscriptions} */ export const getActiveSubscriptions: QueryField< 'getActiveSubscriptions' @@ -2469,7 +2480,7 @@ export const getActiveSubscriptions: QueryField< environmentIOS: sub.environmentIOS ?? null, willExpireSoon: sub.willExpireSoon ?? null, daysUntilExpirationIOS: sub.daysUntilExpirationIOS ?? null, - // 🆕 renewalInfoIOS - subscription lifecycle information (iOS only) + // renewalInfoIOS contains subscription lifecycle information on iOS. renewalInfoIOS: sub.renewalInfoIOS ? { willAutoRenew: sub.renewalInfoIOS.willAutoRenew ?? false, @@ -2512,90 +2523,6 @@ export const getActiveSubscriptions: QueryField< } }; -// OLD IMPLEMENTATION - REPLACED WITH NATIVE CALL -/* -export const getActiveSubscriptions_OLD: QueryField< - 'getActiveSubscriptions' -> = async (subscriptionIds) => { - try { - // Get all available purchases first - const allPurchases = await getAvailablePurchases(); - - // For the critical bug fix: this function was previously returning ALL purchases - // Now we properly filter for subscriptions only - - // In production with real data, Android subscription filtering is done via platform-specific calls - // But for backward compatibility and test support, we also check platform-specific fields - - // Since expirationDateIOS and subscriptionGroupIdIOS are not available in NitroPurchase, - // we need to rely on other indicators or assume all purchases are subscriptions - // when called from getActiveSubscriptions - const purchases = allPurchases; - - // Filter for subscriptions and map to ActiveSubscription format - const subscriptions = purchases - .filter((purchase) => { - // Filter by subscription IDs if provided - if (subscriptionIds && subscriptionIds.length > 0) { - return subscriptionIds.includes(purchase.productId); - } - return true; - }) - .map((purchase): ActiveSubscription => { - // Safe access to platform-specific fields with type guards - const expirationDateIOS = - 'expirationDateIOS' in purchase - ? ((purchase as PurchaseIOS).expirationDateIOS ?? null) - : null; - - const environmentIOS = - 'environmentIOS' in purchase - ? ((purchase as PurchaseIOS).environmentIOS ?? null) - : null; - - const autoRenewingAndroid = - 'autoRenewingAndroid' in purchase || 'isAutoRenewing' in purchase - ? ((purchase as PurchaseAndroid).autoRenewingAndroid ?? - (purchase as PurchaseAndroid).isAutoRenewing) // deprecated - use isAutoRenewing instead - : null; - - // 🆕 Extract renewalInfoIOS if available - const renewalInfoIOS = - 'renewalInfoIOS' in purchase - ? ((purchase as PurchaseIOS).renewalInfoIOS ?? null) - : null; - - return { - productId: purchase.productId, - isActive: true, // If it's in availablePurchases, it's active - // Backend validation fields - use transactionId ?? id for proper field mapping - transactionId: purchase.transactionId ?? purchase.id, - purchaseToken: purchase.purchaseToken, - transactionDate: purchase.transactionDate, - // Platform-specific fields - expirationDateIOS, - autoRenewingAndroid, - environmentIOS, - renewalInfoIOS, - // Convenience fields - willExpireSoon: false, // Would need to calculate based on expiration date - daysUntilExpirationIOS: - expirationDateIOS != null - ? Math.ceil( - (expirationDateIOS - Date.now()) / (1000 * 60 * 60 * 24), - ) - : null, - }; - }); - - return subscriptions; - } catch (error) { - RnIapConsole.error('Failed to get active subscriptions:', error); - const errorJson = parseErrorStringToJsonObj(error); - throw new Error(errorJson.message); - } -}; - /** * Check if the user has any active subscriptions (OpenIAP compliant) * Returns true if the user has at least one active subscription, false otherwise. @@ -2604,7 +2531,7 @@ export const getActiveSubscriptions_OLD: QueryField< * @param subscriptionIds - Optional array of subscription IDs to check * @returns Promise - True if there are active subscriptions * - * @see {@link https://www.openiap.dev/docs/apis/has-active-subscriptions} + * @see {@link https://openiap.dev/docs/apis/has-active-subscriptions} */ export const hasActiveSubscriptions: QueryField< 'hasActiveSubscriptions' @@ -2733,7 +2660,7 @@ const normalizeProductQueryType = ( * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/android/check-alternative-billing-availability-android} + * @see {@link https://openiap.dev/docs/apis/android/check-alternative-billing-availability-android} */ export const checkAlternativeBillingAvailabilityAndroid: MutationField< 'checkAlternativeBillingAvailabilityAndroid' @@ -2775,7 +2702,7 @@ export const checkAlternativeBillingAvailabilityAndroid: MutationField< * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/android/show-alternative-billing-dialog-android} + * @see {@link https://openiap.dev/docs/apis/android/show-alternative-billing-dialog-android} */ export const showAlternativeBillingDialogAndroid: MutationField< 'showAlternativeBillingDialogAndroid' @@ -2814,7 +2741,7 @@ export const showAlternativeBillingDialogAndroid: MutationField< * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/android/create-alternative-billing-token-android} + * @see {@link https://openiap.dev/docs/apis/android/create-alternative-billing-token-android} */ export const createAlternativeBillingTokenAndroid: MutationField< 'createAlternativeBillingTokenAndroid' @@ -2830,40 +2757,6 @@ export const createAlternativeBillingTokenAndroid: MutationField< } }; -/** - * Parameters for launching an external link (Android 8.2.0+). - */ -export interface LaunchExternalLinkParamsAndroid { - /** The billing program (external-content-link or external-offer) */ - billingProgram: BillingProgramAndroid; - /** The external link launch mode */ - launchMode: ExternalLinkLaunchModeAndroid; - /** The type of the external link */ - linkType: ExternalLinkTypeAndroid; - /** The URI where the content will be accessed from */ - linkUri: string; -} - -/** - * Result of checking billing program availability (Android 8.2.0+). - */ -export interface BillingProgramAvailabilityResultAndroid { - /** The billing program that was checked */ - billingProgram: BillingProgramAndroid; - /** Whether the billing program is available for the user */ - isAvailable: boolean; -} - -/** - * Reporting details for external transactions (Android 8.2.0+). - */ -export interface BillingProgramReportingDetailsAndroid { - /** The billing program that the reporting details are associated with */ - billingProgram: BillingProgramAndroid; - /** External transaction token used to report transactions to Google */ - externalTransactionToken: string; -} - /** * Enable a billing program before initConnection (Android only). * Must be called BEFORE initConnection() to configure the BillingClient. @@ -2889,7 +2782,7 @@ export const enableBillingProgramAndroid = ( return; } try { - IAP.instance.enableBillingProgramAndroid(program as any); + IAP.instance.enableBillingProgramAndroid(program); } catch (error) { RnIapConsole.error('Failed to enable billing program:', error); } @@ -2911,7 +2804,7 @@ export const enableBillingProgramAndroid = ( * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/android/is-billing-program-available-android} + * @see {@link https://openiap.dev/docs/apis/android/is-billing-program-available-android} */ export const isBillingProgramAvailableAndroid: MutationField< 'isBillingProgramAvailableAndroid' @@ -2920,9 +2813,7 @@ export const isBillingProgramAvailableAndroid: MutationField< throw new Error('Billing Programs API is only supported on Android'); } try { - const result = await IAP.instance.isBillingProgramAvailableAndroid( - program as any, - ); + const result = await IAP.instance.isBillingProgramAvailableAndroid(program); return { billingProgram: result.billingProgram as unknown as BillingProgramAndroid, isAvailable: result.isAvailable, @@ -2952,7 +2843,7 @@ export const isBillingProgramAvailableAndroid: MutationField< * }); * ``` * - * @see {@link https://www.openiap.dev/docs/apis/android/create-billing-program-reporting-details-android} + * @see {@link https://openiap.dev/docs/apis/android/create-billing-program-reporting-details-android} */ export const createBillingProgramReportingDetailsAndroid: MutationField< 'createBillingProgramReportingDetailsAndroid' @@ -2962,9 +2853,7 @@ export const createBillingProgramReportingDetailsAndroid: MutationField< } try { const result = - await IAP.instance.createBillingProgramReportingDetailsAndroid( - program as any, - ); + await IAP.instance.createBillingProgramReportingDetailsAndroid(program); return { billingProgram: result.billingProgram as unknown as BillingProgramAndroid, externalTransactionToken: result.externalTransactionToken, @@ -2999,7 +2888,7 @@ export const createBillingProgramReportingDetailsAndroid: MutationField< * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/android/launch-external-link-android} + * @see {@link https://openiap.dev/docs/apis/android/launch-external-link-android} */ export const launchExternalLinkAndroid: MutationField< 'launchExternalLinkAndroid' @@ -3009,9 +2898,9 @@ export const launchExternalLinkAndroid: MutationField< } try { return await IAP.instance.launchExternalLinkAndroid({ - billingProgram: params.billingProgram as any, - launchMode: params.launchMode as any, - linkType: params.linkType as any, + billingProgram: params.billingProgram, + launchMode: params.launchMode, + linkType: params.linkType, linkUri: params.linkUri, }); } catch (error) { @@ -3043,7 +2932,7 @@ export const launchExternalLinkAndroid: MutationField< * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios} + * @see {@link https://openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios} */ export const canPresentExternalPurchaseNoticeIOS: QueryField< 'canPresentExternalPurchaseNoticeIOS' @@ -3078,7 +2967,7 @@ export const canPresentExternalPurchaseNoticeIOS: QueryField< * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios} + * @see {@link https://openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios} */ export const presentExternalPurchaseNoticeSheetIOS = async (): Promise => { @@ -3111,7 +3000,7 @@ export const presentExternalPurchaseNoticeSheetIOS = * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/ios/present-external-purchase-link-ios} + * @see {@link https://openiap.dev/docs/apis/ios/present-external-purchase-link-ios} */ export const presentExternalPurchaseLinkIOS: MutationField< 'presentExternalPurchaseLinkIOS' @@ -3120,7 +3009,7 @@ export const presentExternalPurchaseLinkIOS: MutationField< throw new Error('External purchase is only supported on iOS'); } try { - return (await IAP.instance.presentExternalPurchaseLinkIOS(url)) as any; + return await IAP.instance.presentExternalPurchaseLinkIOS(url); } catch (error) { RnIapConsole.error('Failed to present external purchase link:', error); throw error; @@ -3147,7 +3036,7 @@ export const presentExternalPurchaseLinkIOS: MutationField< * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios} + * @see {@link https://openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios} */ export const isEligibleForExternalPurchaseCustomLinkIOS = async (): Promise => { @@ -3184,7 +3073,7 @@ export const isEligibleForExternalPurchaseCustomLinkIOS = * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios} + * @see {@link https://openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios} */ export const getExternalPurchaseCustomLinkTokenIOS = async ( tokenType: ExternalPurchaseCustomLinkTokenTypeIOS, @@ -3224,7 +3113,7 @@ export const getExternalPurchaseCustomLinkTokenIOS = async ( * } * ``` * - * @see {@link https://www.openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios} + * @see {@link https://openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios} */ export const showExternalPurchaseCustomLinkNoticeIOS = async ( noticeType: ExternalPurchaseCustomLinkNoticeTypeIOS, diff --git a/libraries/react-native-iap/src/specs/RnIap.nitro.ts b/libraries/react-native-iap/src/specs/RnIap.nitro.ts index 65f86e25..cdd92767 100644 --- a/libraries/react-native-iap/src/specs/RnIap.nitro.ts +++ b/libraries/react-native-iap/src/specs/RnIap.nitro.ts @@ -35,6 +35,7 @@ import type { ExternalPurchaseCustomLinkTokenTypeIOS, ExternalPurchaseLinkResultIOS, ExternalPurchaseNoticeResultIOS, + DeveloperProvidedBillingDetailsAndroid, MutationFinishTransactionArgs, ProductCommon, PromotionalOfferJwsInputIOS, @@ -147,8 +148,7 @@ export interface NitroReceiptValidationHorizonOptions { userId: VerifyPurchaseHorizonOptions['userId']; } -export interface NitroPurchaseUpdatedListenerOptions - extends PurchaseUpdatedListenerOptions {} +export type NitroPurchaseUpdatedListenerOptions = PurchaseUpdatedListenerOptions; export interface NitroReceiptValidationParams { apple?: NitroReceiptValidationAppleOptions | null; @@ -426,19 +426,6 @@ export interface NitroBillingProgramReportingDetailsAndroid { externalTransactionToken: string; } -/** - * Details provided when user selects developer billing option (Android 8.3.0+) - * Received via DeveloperProvidedBillingListener callback in External Payments flow - */ -export interface DeveloperProvidedBillingDetailsAndroid { - /** - * External transaction token used to report transactions made through developer billing. - * This token must be used when reporting the external transaction to Google Play. - * Must be reported within 24 hours of the transaction. - */ - externalTransactionToken: string; -} - /** * Discount amount details for one-time purchase offers (Android) */ diff --git a/libraries/react-native-iap/src/types.ts b/libraries/react-native-iap/src/types.ts index fc6fef35..fcce2ade 100644 --- a/libraries/react-native-iap/src/types.ts +++ b/libraries/react-native-iap/src/types.ts @@ -587,12 +587,12 @@ export interface LimitedQuantityInfoAndroid { export interface Mutation { /** * Acknowledge a non-consumable purchase. Required within 3 days or Google auto-refunds. - * See: https://www.openiap.dev/docs/apis/android/acknowledge-purchase-android + * See: https://openiap.dev/docs/apis/android/acknowledge-purchase-android */ acknowledgePurchaseAndroid: Promise; /** * Present the refund request sheet (iOS 15+). See also Features → Refund. - * See: https://www.openiap.dev/docs/apis/ios/begin-refund-request-ios + * See: https://openiap.dev/docs/apis/ios/begin-refund-request-ios */ beginRefundRequestIOS?: Promise<(string | null)>; /** @@ -600,17 +600,17 @@ export interface Mutation { * * Returns true if available, false otherwise. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/check-alternative-billing-availability-android + * See: https://openiap.dev/docs/apis/android/check-alternative-billing-availability-android */ checkAlternativeBillingAvailabilityAndroid: Promise; /** * Clear pending transactions in the queue (sandbox helper). - * See: https://www.openiap.dev/docs/apis/ios/clear-transaction-ios + * See: https://openiap.dev/docs/apis/ios/clear-transaction-ios */ clearTransactionIOS: Promise; /** * Consume a consumable purchase so it can be re-bought. - * See: https://www.openiap.dev/docs/apis/android/consume-purchase-android + * See: https://openiap.dev/docs/apis/android/consume-purchase-android */ consumePurchaseAndroid: Promise; /** @@ -620,7 +620,7 @@ export interface Mutation { * * Returns token string, or null if creation failed. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/create-alternative-billing-token-android + * See: https://openiap.dev/docs/apis/android/create-alternative-billing-token-android */ createAlternativeBillingTokenAndroid?: Promise<(string | null)>; /** @@ -629,27 +629,27 @@ export interface Mutation { * * Returns external transaction token needed for reporting external transactions. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/create-billing-program-reporting-details-android + * See: https://openiap.dev/docs/apis/android/create-billing-program-reporting-details-android */ createBillingProgramReportingDetailsAndroid: Promise; /** * Open the platform's subscription management UI. - * See: https://www.openiap.dev/docs/apis/deep-link-to-subscriptions + * See: https://openiap.dev/docs/apis/deep-link-to-subscriptions */ deepLinkToSubscriptions: Promise; /** * Close the store connection and release resources. - * See: https://www.openiap.dev/docs/apis/end-connection + * See: https://openiap.dev/docs/apis/end-connection */ endConnection: Promise; /** * Complete a transaction after server-side verification. Required on Android within 3 days. - * See: https://www.openiap.dev/docs/apis/finish-transaction + * See: https://openiap.dev/docs/apis/finish-transaction */ finishTransaction: Promise; /** * Initialize the store connection. Call before any IAP API. - * See: https://www.openiap.dev/docs/apis/init-connection + * See: https://openiap.dev/docs/apis/init-connection */ initConnection: Promise; /** @@ -659,7 +659,7 @@ export interface Mutation { * Available in Google Play Billing Library 8.2.0+. * Returns availability result with isAvailable flag. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/is-billing-program-available-android + * See: https://openiap.dev/docs/apis/android/is-billing-program-available-android */ isBillingProgramAvailableAndroid: Promise; /** @@ -668,29 +668,29 @@ export interface Mutation { * * Shows Play Store dialog and optionally launches external URL. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/launch-external-link-android + * See: https://openiap.dev/docs/apis/android/launch-external-link-android */ launchExternalLinkAndroid: Promise; /** * Show the App Store offer code redemption sheet. - * See: https://www.openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ presentCodeRedemptionSheetIOS: Promise; /** * Present an external purchase link, StoreKit External (iOS 16+). - * See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-link-ios + * See: https://openiap.dev/docs/apis/ios/present-external-purchase-link-ios */ presentExternalPurchaseLinkIOS: Promise; /** * Present the external purchase notice sheet (iOS 17.4+). * Uses ExternalPurchase.presentNoticeSheet() which returns a token when the user continues. * Reference: https://developer.apple.com/documentation/storekit/externalpurchase/presentnoticesheet() - * See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios + * See: https://openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios */ presentExternalPurchaseNoticeSheetIOS: Promise; /** * Initiate a purchase or subscription flow; rely on events for final state. - * See: https://www.openiap.dev/docs/apis/request-purchase + * See: https://openiap.dev/docs/apis/request-purchase */ requestPurchase?: Promise<(Purchase | Purchase[] | null)>; /** @@ -699,13 +699,13 @@ export interface Mutation { * @deprecated Use promotedProductListenerIOS to receive the productId, * then call requestPurchase with that SKU instead. In StoreKit 2, * promoted products can be purchased directly via the standard purchase flow. - * See: https://www.openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios + * See: https://openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios * @deprecated Use promotedProductListenerIOS + requestPurchase instead */ requestPurchaseOnPromotedProductIOS: Promise; /** * Restore non-consumable and active subscription purchases. - * See: https://www.openiap.dev/docs/apis/restore-purchases + * See: https://openiap.dev/docs/apis/restore-purchases */ restorePurchases: Promise; /** @@ -714,29 +714,29 @@ export interface Mutation { * * Returns true if user accepted, false if user canceled. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/show-alternative-billing-dialog-android + * See: https://openiap.dev/docs/apis/android/show-alternative-billing-dialog-android */ showAlternativeBillingDialogAndroid: Promise; /** * Present the disclosure sheet required before linking out via ExternalPurchaseCustomLink (iOS 18.1+). * Call this after a deliberate customer interaction before linking out to external purchases. * Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/shownotice(type:) - * See: https://www.openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios + * See: https://openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios */ showExternalPurchaseCustomLinkNoticeIOS: Promise; /** * Present the manage-subscriptions sheet and return changed purchases (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/show-manage-subscriptions-ios + * See: https://openiap.dev/docs/apis/ios/show-manage-subscriptions-ios */ showManageSubscriptionsIOS: Promise; /** * Force sync transactions with the App Store (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/sync-ios + * See: https://openiap.dev/docs/apis/ios/sync-ios */ syncIOS: Promise; /** * Deprecated. Validate purchase receipts with the configured providers — use verifyPurchase instead. - * See: https://www.openiap.dev/docs/features/validation#verify-purchase + * See: https://openiap.dev/docs/features/validation#verify-purchase * @deprecated Use verifyPurchase */ validateReceipt: Promise; @@ -746,14 +746,14 @@ export interface Mutation { * + receipt/JWS metadata, VerifyPurchaseResultAndroid carries Play Store * receipt fields (no isValid), and VerifyPurchaseResultHorizon uses success. * Inspect the concrete variant before reading fields. - * See: https://www.openiap.dev/docs/features/validation#verify-purchase + * See: https://openiap.dev/docs/features/validation#verify-purchase */ verifyPurchase: Promise; /** * Verify via a managed provider without standing up your own server. The * PurchaseVerificationProvider enum currently exposes only IAPKit; platform * availability may differ by implementation. - * See: https://www.openiap.dev/docs/features/validation#verify-purchase-with-provider + * See: https://openiap.dev/docs/features/validation#verify-purchase-with-provider */ verifyPurchaseWithProvider: Promise; } @@ -1307,22 +1307,22 @@ export interface Query { /** * Check eligibility for the external purchase notice sheet (iOS 17.4+). * Uses ExternalPurchase.canPresent. - * See: https://www.openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios + * See: https://openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios */ canPresentExternalPurchaseNoticeIOS: Promise; /** * Get the user's current entitlement for a product, using StoreKit 2 (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/current-entitlement-ios + * See: https://openiap.dev/docs/apis/ios/current-entitlement-ios */ currentEntitlementIOS?: Promise<(PurchaseIOS | null)>; /** * Fetch products or subscriptions from the store. - * See: https://www.openiap.dev/docs/apis/fetch-products + * See: https://openiap.dev/docs/apis/fetch-products */ fetchProducts: Promise<(ProductOrSubscription[] | Product[] | ProductSubscription[] | null)>; /** * Get details of all currently active subscriptions (filters by subscriptionIds when provided). - * See: https://www.openiap.dev/docs/apis/get-active-subscriptions + * See: https://openiap.dev/docs/apis/get-active-subscriptions */ getActiveSubscriptions: Promise; /** @@ -1330,92 +1330,92 @@ export interface Query { * Requires the SK2ConsumableTransactionHistory Info.plist key in the host app * for finished consumables to be included (iOS 18+). * Unlike getAvailablePurchases, always returns the iOS-specific PurchaseIOS shape. - * See: https://www.openiap.dev/docs/apis/ios/get-all-transactions-ios + * See: https://openiap.dev/docs/apis/ios/get-all-transactions-ios */ getAllTransactionsIOS: Promise; /** * Fetch the app transaction (iOS 16+). - * See: https://www.openiap.dev/docs/apis/ios/get-app-transaction-ios + * See: https://openiap.dev/docs/apis/ios/get-app-transaction-ios */ getAppTransactionIOS?: Promise<(AppTransaction | null)>; /** * List active purchases for the current user. - * See: https://www.openiap.dev/docs/apis/get-available-purchases + * See: https://openiap.dev/docs/apis/get-available-purchases */ getAvailablePurchases: Promise; /** * Fetch a token for Apple's External Purchase Server reporting API (iOS 18.1+). * Use this token to report transactions made through ExternalPurchaseCustomLink. * Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) - * See: https://www.openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios + * See: https://openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios */ getExternalPurchaseCustomLinkTokenIOS: Promise; /** * List unfinished StoreKit transactions in the queue. - * See: https://www.openiap.dev/docs/apis/ios/get-pending-transactions-ios + * See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios */ getPendingTransactionsIOS: Promise; /** * Read the App Store-promoted product, if any (iOS 11+). - * See: https://www.openiap.dev/docs/apis/ios/get-promoted-product-ios + * See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios */ getPromotedProductIOS?: Promise<(ProductIOS | null)>; /** * Get base64-encoded receipt data (legacy validation). - * See: https://www.openiap.dev/docs/apis/ios/get-receipt-data-ios + * See: https://openiap.dev/docs/apis/ios/get-receipt-data-ios */ getReceiptDataIOS?: Promise<(string | null)>; /** * Return the user's storefront country code. - * See: https://www.openiap.dev/docs/apis/get-storefront + * See: https://openiap.dev/docs/apis/get-storefront */ getStorefront: Promise; /** * Deprecated. Get the current App Store storefront country code — use cross-platform getStorefront instead. - * See: https://www.openiap.dev/docs/apis/ios/get-storefront-ios + * See: https://openiap.dev/docs/apis/ios/get-storefront-ios * @deprecated Use getStorefront */ getStorefrontIOS: Promise; /** * Return the JWS string for a transaction (StoreKit 2). - * See: https://www.openiap.dev/docs/apis/ios/get-transaction-jws-ios + * See: https://openiap.dev/docs/apis/ios/get-transaction-jws-ios */ getTransactionJwsIOS?: Promise<(string | null)>; /** * Check whether the user has any active subscription. - * See: https://www.openiap.dev/docs/apis/has-active-subscriptions + * See: https://openiap.dev/docs/apis/has-active-subscriptions */ hasActiveSubscriptions: Promise; /** * Check eligibility for the custom-link variant of external purchase (iOS 18.1+). * Returns true if the app can use custom external purchase links. * Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/iseligible - * See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios + * See: https://openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios */ isEligibleForExternalPurchaseCustomLinkIOS: Promise; /** * Check intro-offer eligibility for a subscription group. - * See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios + * See: https://openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios */ isEligibleForIntroOfferIOS: Promise; /** * Check whether a transaction's JWS verification passed (StoreKit 2). - * See: https://www.openiap.dev/docs/apis/ios/is-transaction-verified-ios + * See: https://openiap.dev/docs/apis/ios/is-transaction-verified-ios */ isTransactionVerifiedIOS: Promise; /** * Get the latest verified transaction for a product, using StoreKit 2. - * See: https://www.openiap.dev/docs/apis/ios/latest-transaction-ios + * See: https://openiap.dev/docs/apis/ios/latest-transaction-ios */ latestTransactionIOS?: Promise<(PurchaseIOS | null)>; /** * Get subscription status objects from StoreKit 2 (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/subscription-status-ios + * See: https://openiap.dev/docs/apis/ios/subscription-status-ios */ subscriptionStatusIOS: Promise; /** * Deprecated. Legacy App Store receipt validation — use verifyPurchase instead. - * See: https://www.openiap.dev/docs/apis/ios/validate-receipt-ios + * See: https://openiap.dev/docs/apis/ios/validate-receipt-ios * @deprecated Use verifyPurchase */ validateReceiptIOS: Promise; diff --git a/libraries/react-native-iap/tsconfig.build.json b/libraries/react-native-iap/tsconfig.build.json index b6cffe3b..f6c3e217 100644 --- a/libraries/react-native-iap/tsconfig.build.json +++ b/libraries/react-native-iap/tsconfig.build.json @@ -2,7 +2,6 @@ "extends": "./tsconfig", "exclude": [ "example", - "example-expo", "lib", "docs", "plugin/__tests__", diff --git a/libraries/react-native-iap/tsconfig.json b/libraries/react-native-iap/tsconfig.json index afe1aaaf..4810bc80 100644 --- a/libraries/react-native-iap/tsconfig.json +++ b/libraries/react-native-iap/tsconfig.json @@ -27,5 +27,5 @@ "verbatimModuleSyntax": true, "typeRoots": ["./src/types", "./node_modules/@types"] }, - "exclude": ["example-expo", "docs"] + "exclude": ["docs"] } diff --git a/llms-full.txt b/llms-full.txt index 4f628274..561c961e 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-08T10:48:46.777Z +> Generated: 2026-05-16T12:59:43.331Z ## Table of Contents 1. Installation @@ -30,44 +30,47 @@ cd ios && pod install ### Swift (iOS/macOS) ```swift // Swift Package Manager -.package(url: "https://github.com/hyodotdev/openiap.git", from: "1.0.0") +.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.1.9") // CocoaPods -pod 'openiap', '~> 1.0.0' +pod 'openiap', '~> 2.1.9' ``` ### Kotlin (Android) ```kotlin // Gradle (build.gradle.kts) -implementation("io.github.hyochan.openiap:openiap-google:1.0.0") +implementation("io.github.hyochan.openiap:openiap-google:2.1.5") // For Meta Horizon OS -implementation("io.github.hyochan.openiap:openiap-google-horizon:1.0.0") +implementation("io.github.hyochan.openiap:openiap-google-horizon:2.1.5") ``` ### Flutter -```yaml -# pubspec.yaml -dependencies: - flutter_inapp_purchase: ^5.0.0 +```bash +flutter pub add flutter_inapp_purchase ``` ### Godot -Download `godot-iap` from the Godot Asset Library or GitHub Releases, extract -it to `addons/godot-iap/`, then enable the plugin in Project Settings. +Download `godot-iap-2.2.10.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.kmpiap:library:1.3.8") + implementation("io.github.hyochan:kmp-iap:2.2.8") } ``` +Use the latest version from Maven Central: +https://central.sonatype.com/artifact/io.github.hyochan/kmp-iap + ### .NET MAUI -```xml - +```bash +dotnet add package OpenIap.Maui ``` +Current NuGet package version: 1.0.4 + Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. --- @@ -103,7 +106,7 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. - Public surface: snake_case functions and Godot signals matching OpenIAP. ### kmp-iap -- Package: `io.github.hyochan.kmpiap:library`. +- Package: `io.github.hyochan:kmp-iap`. - Implementation: Kotlin Multiplatform common API with Flow-based events, Android implementation, and iOS cinterop through the OpenIAP ObjC facade. - Public surface: `KmpIAP` / shared instance resolver methods and flows. @@ -601,7 +604,7 @@ Google Play Billing Library enables in-app purchases and subscriptions on Androi | 8.2.1 | 2025-12-15 | Bug fix for `isBillingProgramAvailableAsync()` and `createBillingProgramReportingDetailsAsync()` | | 8.3 | 2025-12-23 | External Payments program (Japan only), developer billing options | -**Current Version**: 8.3.0 (as of January 2026) +**Current Version**: 8.3.0 (as of April 2026) ## Core Classes diff --git a/llms.txt b/llms.txt index 9b6b2381..45cfb2e2 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-08T10:48:46.778Z +> Generated: 2026-05-16T12:59:43.331Z ## Installation @@ -19,35 +19,36 @@ npm install react-native-iap ### Native ```swift // Swift Package Manager -.package(url: "https://github.com/hyodotdev/openiap.git", from: "1.0.0") +.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.1.9") ``` ```kotlin // Gradle -implementation("io.github.hyochan.openiap:openiap-google:1.0.0") +implementation("io.github.hyochan.openiap:openiap-google:2.1.5") ``` -```yaml +```bash # Flutter -dependencies: - flutter_inapp_purchase: ^5.0.0 +flutter pub add flutter_inapp_purchase ``` ```gdscript # Godot -# Install godot-iap to addons/godot-iap and enable the plugin +# Install godot-iap 2.2.10 to addons/godot-iap and enable the plugin ``` ```kotlin // Kotlin Multiplatform -implementation("io.github.hyochan.kmpiap:library:1.3.8") +implementation("io.github.hyochan:kmp-iap:2.2.8") ``` -```xml - - +```bash +# .NET MAUI +dotnet add package OpenIap.Maui ``` +Current NuGet package version: 1.0.4 + ## Framework Libraries - `expo-iap`: Expo Modules wrapper, same OpenIAP API as React Native. diff --git a/package.json b/package.json index e4b3d772..c015ea62 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,10 @@ "typescript": "^5.9.2" }, "overrides": { - "csstype": "3.2.3" + "csstype": "3.2.3", + "fast-uri": "3.1.2", + "hono": "4.12.18", + "ip-address": "10.2.0" }, "packageManager": "bun@1.3.13" } diff --git a/packages/apple/.gitignore b/packages/apple/.gitignore index 8ff2363d..68a17815 100644 --- a/packages/apple/.gitignore +++ b/packages/apple/.gitignore @@ -5,6 +5,7 @@ .build/ .swiftpm/ Package.resolved +workspace-state.json # xcodegen output for the wrapper project that produces OpenIAP.xcframework. # project.yml is the SoT; the .xcodeproj is regenerated by build-xcframework.sh. @@ -41,4 +42,4 @@ Thumbs.db *.temp *~.nib *.swp -*.log \ No newline at end of file +*.log diff --git a/packages/apple/CONTRIBUTING.md b/packages/apple/CONTRIBUTING.md index 03a11aef..40700cf0 100644 --- a/packages/apple/CONTRIBUTING.md +++ b/packages/apple/CONTRIBUTING.md @@ -8,7 +8,7 @@ Thank you for your interest in contributing! We love your input and appreciate y 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 3. Make your changes 4. Run tests (`swift test`) -5. Commit your changes (`git commit -m 'Add amazing feature'`) +5. Commit your changes (`git commit -m 'feat(apple): add amazing feature'`) 6. Push to your branch (`git push origin feature/amazing-feature`) 7. Open a Pull Request @@ -16,8 +16,8 @@ Thank you for your interest in contributing! We love your input and appreciate y ```bash # Clone your fork -git clone https://github.com/YOUR_USERNAME/openiap-apple.git -cd openiap-apple +git clone https://github.com/YOUR_USERNAME/openiap.git +cd openiap/packages/apple # Open in Xcode open Package.swift diff --git a/packages/apple/Example/OpenIapExample/Screens/WebhookStreamScreen.swift b/packages/apple/Example/OpenIapExample/Screens/WebhookStreamScreen.swift index d7c20aa8..b1cd8383 100644 --- a/packages/apple/Example/OpenIapExample/Screens/WebhookStreamScreen.swift +++ b/packages/apple/Example/OpenIapExample/Screens/WebhookStreamScreen.swift @@ -30,7 +30,7 @@ struct WebhookStreamScreen: View { VStack(alignment: .leading, spacing: 8) { Text("SSE /v1/webhooks/stream/{apiKey}") .font(.headline) - Text("api key: \(apiKey.isEmpty ? "MISSING" : "\(apiKey.prefix(8))...")") + Text("api key: \(apiKey.isEmpty ? "MISSING" : "CONFIGURED")") .font(.caption) .foregroundColor(.secondary) } diff --git a/packages/apple/Example/workspace-state.json b/packages/apple/Example/workspace-state.json deleted file mode 100644 index 5b0ae04d..00000000 --- a/packages/apple/Example/workspace-state.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "object" : { - "artifacts" : [ - - ], - "dependencies" : [ - { - "basedOn" : null, - "packageRef" : { - "identity" : "openiap-apple", - "kind" : "fileSystem", - "location" : "/Users/hyo/Github/hyodotdev/openiap-apple", - "name" : "OpenIAP" - }, - "state" : { - "name" : "fileSystem", - "path" : "/Users/hyo/Github/hyodotdev/openiap-apple" - }, - "subpath" : "openiap-apple" - } - ], - "prebuilts" : [ - - ] - }, - "version" : 7 -} \ No newline at end of file diff --git a/packages/apple/README.md b/packages/apple/README.md index e24a3e45..6266fbcb 100644 --- a/packages/apple/README.md +++ b/packages/apple/README.md @@ -50,7 +50,7 @@ Add to your `Package.swift`: ```swift dependencies: [ - .package(url: "https://github.com/hyodotdev/openiap.git", from: "$version") + .package(url: "https://github.com/hyodotdev/openiap.git", from: "") ] ``` @@ -59,10 +59,10 @@ dependencies: [ Add to your `Podfile`: ```ruby -pod 'openiap', '~> $version' +pod 'openiap', '~> ' ``` -> Check [`openiap-versions.json`](../../openiap-versions.json) for the current version. +Use the latest version from the Swift Package / CocoaPods badges above. ## Quick Start diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index 8cde48d8..b1ac4d36 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -2502,22 +2502,22 @@ public enum VerifyPurchaseResult: Codable { /// GraphQL root mutation operations. public protocol MutationResolver { /// Acknowledge a non-consumable purchase. Required within 3 days or Google auto-refunds. - /// See: https://www.openiap.dev/docs/apis/android/acknowledge-purchase-android + /// See: https://openiap.dev/docs/apis/android/acknowledge-purchase-android func acknowledgePurchaseAndroid(_ purchaseToken: String) async throws -> Bool /// Present the refund request sheet (iOS 15+). See also Features → Refund. - /// See: https://www.openiap.dev/docs/apis/ios/begin-refund-request-ios + /// See: https://openiap.dev/docs/apis/ios/begin-refund-request-ios func beginRefundRequestIOS(_ sku: String) async throws -> String? /// Check whether alternative billing is available for the user. Step 1 of the alternative billing flow. /// /// Returns true if available, false otherwise. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/check-alternative-billing-availability-android + /// See: https://openiap.dev/docs/apis/android/check-alternative-billing-availability-android func checkAlternativeBillingAvailabilityAndroid() async throws -> Bool /// Clear pending transactions in the queue (sandbox helper). - /// See: https://www.openiap.dev/docs/apis/ios/clear-transaction-ios + /// See: https://openiap.dev/docs/apis/ios/clear-transaction-ios func clearTransactionIOS() async throws -> Bool /// Consume a consumable purchase so it can be re-bought. - /// See: https://www.openiap.dev/docs/apis/android/consume-purchase-android + /// See: https://openiap.dev/docs/apis/android/consume-purchase-android func consumePurchaseAndroid(_ purchaseToken: String) async throws -> Bool /// Create a reporting token for an alternative billing flow. Step 3 of the alternative billing flow. /// Must be called AFTER successful payment in your payment system. @@ -2525,26 +2525,26 @@ public protocol MutationResolver { /// /// Returns token string, or null if creation failed. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/create-alternative-billing-token-android + /// See: https://openiap.dev/docs/apis/android/create-alternative-billing-token-android func createAlternativeBillingTokenAndroid() async throws -> String? /// Create the reporting payload Google requires after a Developer-Provided Billing transaction (Play Billing 8.3.0+). /// Replaces the deprecated createExternalOfferReportingDetailsAsync API. /// /// Returns external transaction token needed for reporting external transactions. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/create-billing-program-reporting-details-android + /// See: https://openiap.dev/docs/apis/android/create-billing-program-reporting-details-android func createBillingProgramReportingDetailsAndroid(_ program: BillingProgramAndroid) async throws -> BillingProgramReportingDetailsAndroid /// Open the platform's subscription management UI. - /// See: https://www.openiap.dev/docs/apis/deep-link-to-subscriptions + /// See: https://openiap.dev/docs/apis/deep-link-to-subscriptions func deepLinkToSubscriptions(_ options: DeepLinkOptions?) async throws -> Void /// Close the store connection and release resources. - /// See: https://www.openiap.dev/docs/apis/end-connection + /// See: https://openiap.dev/docs/apis/end-connection func endConnection() async throws -> Bool /// Complete a transaction after server-side verification. Required on Android within 3 days. - /// See: https://www.openiap.dev/docs/apis/finish-transaction + /// See: https://openiap.dev/docs/apis/finish-transaction func finishTransaction(purchase: PurchaseInput, isConsumable: Bool?) async throws -> Void /// Initialize the store connection. Call before any IAP API. - /// See: https://www.openiap.dev/docs/apis/init-connection + /// See: https://openiap.dev/docs/apis/init-connection func initConnection(_ config: InitConnectionConfig?) async throws -> Bool /// Check whether a billing program (e.g., External Payments) is available for the current user. /// Replaces the deprecated isExternalOfferAvailableAsync API. @@ -2552,71 +2552,71 @@ public protocol MutationResolver { /// Available in Google Play Billing Library 8.2.0+. /// Returns availability result with isAvailable flag. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/is-billing-program-available-android + /// See: https://openiap.dev/docs/apis/android/is-billing-program-available-android func isBillingProgramAvailableAndroid(_ program: BillingProgramAndroid) async throws -> BillingProgramAvailabilityResultAndroid /// Launch an external content/offer link from inside the Billing Programs flow (Play Billing 8.2.0+). /// Replaces the deprecated showExternalOfferInformationDialog API. /// /// Shows Play Store dialog and optionally launches external URL. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/launch-external-link-android + /// See: https://openiap.dev/docs/apis/android/launch-external-link-android func launchExternalLinkAndroid(_ params: LaunchExternalLinkParamsAndroid) async throws -> Bool /// Show the App Store offer code redemption sheet. - /// See: https://www.openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios func presentCodeRedemptionSheetIOS() async throws -> Bool /// Present an external purchase link, StoreKit External (iOS 16+). - /// See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-link-ios + /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-link-ios func presentExternalPurchaseLinkIOS(_ url: String) async throws -> ExternalPurchaseLinkResultIOS /// Present the external purchase notice sheet (iOS 17.4+). /// Uses ExternalPurchase.presentNoticeSheet() which returns a token when the user continues. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchase/presentnoticesheet() - /// See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios + /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios func presentExternalPurchaseNoticeSheetIOS() async throws -> ExternalPurchaseNoticeResultIOS /// Initiate a purchase or subscription flow; rely on events for final state. - /// See: https://www.openiap.dev/docs/apis/request-purchase + /// See: https://openiap.dev/docs/apis/request-purchase func requestPurchase(_ params: RequestPurchaseProps) async throws -> RequestPurchaseResult? /// Buy the currently promoted product. /// /// @deprecated Use promotedProductListenerIOS to receive the productId, /// then call requestPurchase with that SKU instead. In StoreKit 2, /// promoted products can be purchased directly via the standard purchase flow. - /// See: https://www.openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios + /// See: https://openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios func requestPurchaseOnPromotedProductIOS() async throws -> Bool /// Restore non-consumable and active subscription purchases. - /// See: https://www.openiap.dev/docs/apis/restore-purchases + /// See: https://openiap.dev/docs/apis/restore-purchases func restorePurchases() async throws -> Void /// Display Google's alternative billing information dialog. Step 2 of the alternative billing flow. /// Must be called BEFORE processing payment in your payment system. /// /// Returns true if user accepted, false if user canceled. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/show-alternative-billing-dialog-android + /// See: https://openiap.dev/docs/apis/android/show-alternative-billing-dialog-android func showAlternativeBillingDialogAndroid() async throws -> Bool /// Present the disclosure sheet required before linking out via ExternalPurchaseCustomLink (iOS 18.1+). /// Call this after a deliberate customer interaction before linking out to external purchases. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/shownotice(type:) - /// See: https://www.openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios + /// See: https://openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios func showExternalPurchaseCustomLinkNoticeIOS(_ noticeType: ExternalPurchaseCustomLinkNoticeTypeIOS) async throws -> ExternalPurchaseCustomLinkNoticeResultIOS /// Present the manage-subscriptions sheet and return changed purchases (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/show-manage-subscriptions-ios + /// See: https://openiap.dev/docs/apis/ios/show-manage-subscriptions-ios func showManageSubscriptionsIOS() async throws -> [PurchaseIOS] /// Force sync transactions with the App Store (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/sync-ios + /// See: https://openiap.dev/docs/apis/ios/sync-ios func syncIOS() async throws -> Bool /// Deprecated. Validate purchase receipts with the configured providers — use verifyPurchase instead. - /// See: https://www.openiap.dev/docs/features/validation#verify-purchase + /// See: https://openiap.dev/docs/features/validation#verify-purchase func validateReceipt(_ options: VerifyPurchaseProps) async throws -> VerifyPurchaseResult /// Verify a purchase against your own backend. Returns a platform-specific /// variant of VerifyPurchaseResult — VerifyPurchaseResultIOS exposes isValid /// + receipt/JWS metadata, VerifyPurchaseResultAndroid carries Play Store /// receipt fields (no isValid), and VerifyPurchaseResultHorizon uses success. /// Inspect the concrete variant before reading fields. - /// See: https://www.openiap.dev/docs/features/validation#verify-purchase + /// See: https://openiap.dev/docs/features/validation#verify-purchase func verifyPurchase(_ options: VerifyPurchaseProps) async throws -> VerifyPurchaseResult /// Verify via a managed provider without standing up your own server. The /// PurchaseVerificationProvider enum currently exposes only IAPKit; platform /// availability may differ by implementation. - /// See: https://www.openiap.dev/docs/features/validation#verify-purchase-with-provider + /// See: https://openiap.dev/docs/features/validation#verify-purchase-with-provider func verifyPurchaseWithProvider(_ options: VerifyPurchaseWithProviderProps) async throws -> VerifyPurchaseWithProviderResult } @@ -2624,74 +2624,74 @@ public protocol MutationResolver { public protocol QueryResolver { /// Check eligibility for the external purchase notice sheet (iOS 17.4+). /// Uses ExternalPurchase.canPresent. - /// See: https://www.openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios + /// See: https://openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios func canPresentExternalPurchaseNoticeIOS() async throws -> Bool /// Get the user's current entitlement for a product, using StoreKit 2 (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/current-entitlement-ios + /// See: https://openiap.dev/docs/apis/ios/current-entitlement-ios func currentEntitlementIOS(_ sku: String) async throws -> PurchaseIOS? /// Fetch products or subscriptions from the store. - /// See: https://www.openiap.dev/docs/apis/fetch-products + /// See: https://openiap.dev/docs/apis/fetch-products func fetchProducts(_ params: ProductRequest) async throws -> FetchProductsResult /// Get details of all currently active subscriptions (filters by subscriptionIds when provided). - /// See: https://www.openiap.dev/docs/apis/get-active-subscriptions + /// See: https://openiap.dev/docs/apis/get-active-subscriptions func getActiveSubscriptions(_ subscriptionIds: [String]?) async throws -> [ActiveSubscription] /// List every StoreKit transaction (finished + unfinished) for the current user. /// Requires the SK2ConsumableTransactionHistory Info.plist key in the host app /// for finished consumables to be included (iOS 18+). /// Unlike getAvailablePurchases, always returns the iOS-specific PurchaseIOS shape. - /// See: https://www.openiap.dev/docs/apis/ios/get-all-transactions-ios + /// See: https://openiap.dev/docs/apis/ios/get-all-transactions-ios func getAllTransactionsIOS() async throws -> [PurchaseIOS] /// Fetch the app transaction (iOS 16+). - /// See: https://www.openiap.dev/docs/apis/ios/get-app-transaction-ios + /// See: https://openiap.dev/docs/apis/ios/get-app-transaction-ios func getAppTransactionIOS() async throws -> AppTransaction? /// List active purchases for the current user. - /// See: https://www.openiap.dev/docs/apis/get-available-purchases + /// See: https://openiap.dev/docs/apis/get-available-purchases func getAvailablePurchases(_ options: PurchaseOptions?) async throws -> [Purchase] /// Fetch a token for Apple's External Purchase Server reporting API (iOS 18.1+). /// Use this token to report transactions made through ExternalPurchaseCustomLink. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) - /// See: https://www.openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios + /// See: https://openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios func getExternalPurchaseCustomLinkTokenIOS(_ tokenType: ExternalPurchaseCustomLinkTokenTypeIOS) async throws -> ExternalPurchaseCustomLinkTokenResultIOS /// List unfinished StoreKit transactions in the queue. - /// See: https://www.openiap.dev/docs/apis/ios/get-pending-transactions-ios + /// See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios func getPendingTransactionsIOS() async throws -> [PurchaseIOS] /// Read the App Store-promoted product, if any (iOS 11+). - /// See: https://www.openiap.dev/docs/apis/ios/get-promoted-product-ios + /// See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios func getPromotedProductIOS() async throws -> ProductIOS? /// Get base64-encoded receipt data (legacy validation). - /// See: https://www.openiap.dev/docs/apis/ios/get-receipt-data-ios + /// See: https://openiap.dev/docs/apis/ios/get-receipt-data-ios func getReceiptDataIOS() async throws -> String? /// Return the user's storefront country code. - /// See: https://www.openiap.dev/docs/apis/get-storefront + /// See: https://openiap.dev/docs/apis/get-storefront func getStorefront() async throws -> String /// Deprecated. Get the current App Store storefront country code — use cross-platform getStorefront instead. - /// See: https://www.openiap.dev/docs/apis/ios/get-storefront-ios + /// See: https://openiap.dev/docs/apis/ios/get-storefront-ios func getStorefrontIOS() async throws -> String /// Return the JWS string for a transaction (StoreKit 2). - /// See: https://www.openiap.dev/docs/apis/ios/get-transaction-jws-ios + /// See: https://openiap.dev/docs/apis/ios/get-transaction-jws-ios func getTransactionJwsIOS(_ sku: String) async throws -> String? /// Check whether the user has any active subscription. - /// See: https://www.openiap.dev/docs/apis/has-active-subscriptions + /// See: https://openiap.dev/docs/apis/has-active-subscriptions func hasActiveSubscriptions(_ subscriptionIds: [String]?) async throws -> Bool /// Check eligibility for the custom-link variant of external purchase (iOS 18.1+). /// Returns true if the app can use custom external purchase links. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/iseligible - /// See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios + /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios func isEligibleForExternalPurchaseCustomLinkIOS() async throws -> Bool /// Check intro-offer eligibility for a subscription group. - /// See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios + /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios func isEligibleForIntroOfferIOS(_ groupID: String) async throws -> Bool /// Check whether a transaction's JWS verification passed (StoreKit 2). - /// See: https://www.openiap.dev/docs/apis/ios/is-transaction-verified-ios + /// See: https://openiap.dev/docs/apis/ios/is-transaction-verified-ios func isTransactionVerifiedIOS(_ sku: String) async throws -> Bool /// Get the latest verified transaction for a product, using StoreKit 2. - /// See: https://www.openiap.dev/docs/apis/ios/latest-transaction-ios + /// See: https://openiap.dev/docs/apis/ios/latest-transaction-ios func latestTransactionIOS(_ sku: String) async throws -> PurchaseIOS? /// Get subscription status objects from StoreKit 2 (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/subscription-status-ios + /// See: https://openiap.dev/docs/apis/ios/subscription-status-ios func subscriptionStatusIOS(_ sku: String) async throws -> [SubscriptionStatusIOS] /// Deprecated. Legacy App Store receipt validation — use verifyPurchase instead. - /// See: https://www.openiap.dev/docs/apis/ios/validate-receipt-ios + /// See: https://openiap.dev/docs/apis/ios/validate-receipt-ios func validateReceiptIOS(_ options: VerifyPurchaseProps) async throws -> VerifyPurchaseResultIOS } diff --git a/packages/apple/Sources/OpenIapModule+ObjC.swift b/packages/apple/Sources/OpenIapModule+ObjC.swift index c69d94aa..ef2b2d7e 100644 --- a/packages/apple/Sources/OpenIapModule+ObjC.swift +++ b/packages/apple/Sources/OpenIapModule+ObjC.swift @@ -159,6 +159,37 @@ import StoreKit } } + @objc func requestPurchaseWithPayload( + _ payload: [String: Any], + completion: @escaping (Any?, Error?) -> Void + ) { + Task { + do { + let props = try OpenIapSerialization.requestPurchaseProps(from: payload) + let result = try await requestPurchase(props) + + switch result { + case .purchase(let purchase): + if let purchase = purchase { + completion(OpenIapSerialization.purchase(purchase), nil) + } else { + completion(nil, nil) + } + case .purchases(let purchases): + if let firstPurchase = purchases?.first { + completion(OpenIapSerialization.purchase(firstPurchase), nil) + } else { + completion(nil, nil) + } + case .none: + completion(nil, nil) + } + } catch { + completion(nil, error) + } + } + } + @objc func requestSubscriptionWithSku( _ sku: String, offer: [String: Any]?, diff --git a/packages/apple/Sources/OpenIapModule.swift b/packages/apple/Sources/OpenIapModule.swift index 66fc792f..0ba78d72 100644 --- a/packages/apple/Sources/OpenIapModule.swift +++ b/packages/apple/Sources/OpenIapModule.swift @@ -56,7 +56,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { /// - Note: This wraps `OpenIapStoreKit2.initialize()`. Safe to call multiple times — the /// second call is a no-op. /// - /// See: https://www.openiap.dev/docs/apis/init-connection + /// See: https://openiap.dev/docs/apis/init-connection public func initConnection() async throws -> Bool { while true { if let endTask = connection.currentEndTask() { @@ -95,7 +95,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Close the store connection and release resources. - /// See: https://www.openiap.dev/docs/apis/end-connection + /// See: https://openiap.dev/docs/apis/end-connection public func endConnection() async throws -> Bool { let task = connection.makeEndTask { [weak self] in guard let self else { return } @@ -118,7 +118,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { /// - Note: This is a regular promise-based call. Do not confuse with `request*` APIs, /// which are event-based. /// - /// See: https://www.openiap.dev/docs/apis/fetch-products + /// See: https://openiap.dev/docs/apis/fetch-products public func fetchProducts(_ params: ProductRequest) async throws -> FetchProductsResult { guard !params.skus.isEmpty else { let error = makePurchaseError(code: .emptySkuList) @@ -218,7 +218,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Read the App Store-promoted product, if any. - /// See: https://www.openiap.dev/docs/apis/ios/get-promoted-product-ios + /// See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios public func getPromotedProductIOS() async throws -> ProductIOS? { // iOS-only: Promoted in-app purchases (App Store promotional purchases) only available on iOS // Reference: https://developer.apple.com/documentation/storekit/promoting-in-app-purchases @@ -267,7 +267,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { /// - Throws: Synchronous rejections from StoreKit (e.g. user cancel before sheet, not prepared). /// - Warning: Event-based. Listen via `purchaseUpdatedListener` / `purchaseErrorListener`. /// - /// See: https://www.openiap.dev/docs/apis/request-purchase + /// See: https://openiap.dev/docs/apis/request-purchase public func requestPurchase(_ params: RequestPurchaseProps) async throws -> RequestPurchaseResult? { try await ensureConnection() let iosProps = try resolveIOSPurchaseProps(from: params) @@ -417,14 +417,42 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Buy the currently promoted product. - /// See: https://www.openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios + /// See: https://openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios @available(*, deprecated, message: "Use promotedProductListenerIOS + requestPurchase instead") public func requestPurchaseOnPromotedProductIOS() async throws -> Bool { + #if os(iOS) + guard let sku = await state.promotedProductIdentifier() else { + return false + } + + try await ensureConnection() + let product = try await storeProduct(for: sku) + let request: RequestPurchaseProps + + switch product.type { + case .autoRenewable, .nonRenewable: + let iosProps = RequestSubscriptionIosProps(sku: sku) + request = RequestPurchaseProps( + request: .subscription(RequestSubscriptionPropsByPlatforms(android: nil, ios: iosProps)), + type: .subs + ) + default: + let iosProps = RequestPurchaseIosProps(sku: sku) + request = RequestPurchaseProps( + request: .purchase(RequestPurchasePropsByPlatforms(android: nil, ios: iosProps)), + type: .inApp + ) + } + + _ = try await requestPurchase(request) + return true + #else throw makePurchaseError(code: .featureNotSupported) + #endif } /// Restore non-consumable and active subscription purchases. - /// See: https://www.openiap.dev/docs/apis/restore-purchases + /// See: https://openiap.dev/docs/apis/restore-purchases public func restorePurchases() async throws -> Void { _ = try await syncIOS() } @@ -440,7 +468,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { /// - Returns: An array of `Purchase` values matching the selected scope. /// - Throws: When the StoreKit query fails. /// - /// See: https://www.openiap.dev/docs/apis/get-available-purchases + /// See: https://openiap.dev/docs/apis/get-available-purchases public func getAvailablePurchases(_ options: PurchaseOptions?) async throws -> [Purchase] { try await ensureConnection() let onlyActive = options?.onlyIncludeActiveItemsIOS ?? false @@ -476,7 +504,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { /// `Transaction.all` and returns the iOS-specific `PurchaseIOS` shape rather than /// the cross-platform `Purchase` type. /// - /// See: https://www.openiap.dev/docs/apis/ios/get-all-transactions-ios + /// See: https://openiap.dev/docs/apis/ios/get-all-transactions-ios public func getAllTransactionsIOS() async throws -> [PurchaseIOS] { try await ensureConnection() var transactions: [PurchaseIOS] = [] @@ -513,7 +541,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { /// - Important: iOS unfinished transactions replay on every app launch. (Android purchases /// must be acknowledged within 3 days, but that path lives in the Android module.) /// - /// See: https://www.openiap.dev/docs/apis/finish-transaction + /// See: https://openiap.dev/docs/apis/finish-transaction public func finishTransaction(purchase: PurchaseInput, isConsumable: Bool?) async throws -> Void { try await ensureConnection() let identifier = purchase.id @@ -560,7 +588,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// List unfinished StoreKit transactions. - /// See: https://www.openiap.dev/docs/apis/ios/get-pending-transactions-ios + /// See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios public func getPendingTransactionsIOS() async throws -> [PurchaseIOS] { try await ensureConnection() let snapshot = await state.pendingSnapshot() @@ -572,7 +600,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Clear pending transactions in the queue (sandbox helper). - /// See: https://www.openiap.dev/docs/apis/ios/clear-transaction-ios + /// See: https://openiap.dev/docs/apis/ios/clear-transaction-ios public func clearTransactionIOS() async throws -> Bool { try await ensureConnection() for await result in Transaction.unfinished { @@ -588,7 +616,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Check whether a transaction's JWS verification passed. - /// See: https://www.openiap.dev/docs/apis/ios/is-transaction-verified-ios + /// See: https://openiap.dev/docs/apis/ios/is-transaction-verified-ios public func isTransactionVerifiedIOS(sku: String) async throws -> Bool { try await ensureConnection() let product = try await storeProduct(for: sku) @@ -602,7 +630,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Return the JWS string for a transaction. - /// See: https://www.openiap.dev/docs/apis/ios/get-transaction-jws-ios + /// See: https://openiap.dev/docs/apis/ios/get-transaction-jws-ios public func getTransactionJwsIOS(sku: String) async throws -> String? { try await ensureConnection() let product = try await storeProduct(for: sku) @@ -617,7 +645,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { // MARK: - Validation /// Get base64 receipt data (legacy validation). - /// See: https://www.openiap.dev/docs/apis/ios/get-receipt-data-ios + /// See: https://openiap.dev/docs/apis/ios/get-receipt-data-ios public func getReceiptDataIOS() async throws -> String? { guard let receiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: receiptURL.path) else { @@ -628,7 +656,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Deprecated. Legacy App Store receipt validation. Use `verifyPurchase` instead. - /// See: https://www.openiap.dev/docs/apis/ios/validate-receipt-ios + /// See: https://openiap.dev/docs/apis/ios/validate-receipt-ios @available(*, deprecated, message: "Use verifyPurchase") public func validateReceiptIOS(_ props: VerifyPurchaseProps) async throws -> VerifyPurchaseResultIOS { try await performVerifyPurchaseIOS(props) @@ -669,14 +697,14 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Deprecated. Use verifyPurchase instead — same input/output shape. - /// See: https://www.openiap.dev/docs/apis/validate-receipt + /// See: https://openiap.dev/docs/apis/validate-receipt @available(*, deprecated, message: "Use verifyPurchase") public func validateReceipt(_ props: VerifyPurchaseProps) async throws -> VerifyPurchaseResult { try await verifyPurchase(props) } /// Verify a purchase against your own backend (returns isValid + raw store metadata). - /// See: https://www.openiap.dev/docs/features/validation#verify-purchase + /// See: https://openiap.dev/docs/features/validation#verify-purchase public func verifyPurchase(_ props: VerifyPurchaseProps) async throws -> VerifyPurchaseResult { try await ensureConnection() let iosResult = try await performVerifyPurchaseIOS(props) @@ -684,7 +712,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Verify via a managed provider (currently IAPKit; the PurchaseVerificationProvider enum exposes only Iapkit today). - /// See: https://www.openiap.dev/docs/features/validation#verify-purchase-with-provider + /// 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 { @@ -724,13 +752,8 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { // Log request details for debugging OpenIapLog.debug("IAPKit request URL: \(url.absoluteString)") - if let requestBody = String(data: body, encoding: .utf8) { - // Truncate JWS for readability (keep first/last 50 chars) - let truncatedBody = requestBody.count > 200 - ? String(requestBody.prefix(100)) + "..." + String(requestBody.suffix(50)) - : requestBody - OpenIapLog.debug("IAPKit request body: \(truncatedBody)") - } + 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 { @@ -738,7 +761,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } guard (200...299).contains(httpResponse.statusCode) else { let responseBody = String(data: data, encoding: .utf8) ?? "" - OpenIapLog.warn("verifyPurchaseWithProvider failed (HTTP \(httpResponse.statusCode)): \(responseBody)") + 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), @@ -748,14 +771,13 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { throw makePurchaseError(code: .receiptFailed, message: errorMessage) } - // Log raw response for debugging - let jsonString = String(data: data, encoding: .utf8) ?? "" - OpenIapLog.info("IAPKit raw response: \(jsonString)") + // Log only response metadata; the body can contain receipt details. + OpenIapLog.debug("IAPKit verification response received: bytes=\(data.count)") // 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. Raw: \(jsonString)") + OpenIapLog.warn("Failed to parse IAPKit verification response") throw makePurchaseError(code: .receiptFailed, message: "Unable to parse verification response") } @@ -853,7 +875,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { // MARK: - Store Information /// Return the user's storefront country code. - /// See: https://www.openiap.dev/docs/apis/get-storefront + /// See: https://openiap.dev/docs/apis/get-storefront public func getStorefront() async throws -> String { try await ensureConnection() guard let storefront = await Storefront.current else { @@ -865,7 +887,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Deprecated. Use cross-platform `getStorefront` instead. - /// See: https://www.openiap.dev/docs/apis/ios/get-storefront-ios + /// See: https://openiap.dev/docs/apis/ios/get-storefront-ios public func getStorefrontIOS() async throws -> String { try await getStorefront() } @@ -874,7 +896,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { /// - Note: Available on iOS 16.0+, macOS 14.0+, tvOS 16.0+, watchOS 9.0+ /// - SeeAlso: https://developer.apple.com/documentation/storekit/apptransaction /// - /// See: https://www.openiap.dev/docs/apis/ios/get-app-transaction-ios + /// See: https://openiap.dev/docs/apis/ios/get-app-transaction-ios @available(iOS 16.0, macOS 14.0, tvOS 16.0, watchOS 9.0, *) public func getAppTransactionIOS() async throws -> AppTransaction? { try await ensureConnection() @@ -890,7 +912,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { // MARK: - Subscription Management /// Get details of all currently active subscriptions. - /// See: https://www.openiap.dev/docs/apis/get-active-subscriptions + /// See: https://openiap.dev/docs/apis/get-active-subscriptions public func getActiveSubscriptions(_ subscriptionIds: [String]?) async throws -> [ActiveSubscription] { try await ensureConnection() var allSubscriptions: [ActiveSubscription] = [] @@ -952,7 +974,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Check whether the user has any active subscription. - /// See: https://www.openiap.dev/docs/apis/has-active-subscriptions + /// See: https://openiap.dev/docs/apis/has-active-subscriptions public func hasActiveSubscriptions(_ subscriptionIds: [String]?) async throws -> Bool { let subscriptions = try await getActiveSubscriptions(subscriptionIds) return subscriptions.contains { $0.isActive } @@ -962,7 +984,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { /// - Note: Available on iOS 15.0+, iPadOS 15.0+, Mac Catalyst 15.0+, macOS 14.0+, visionOS 1.0+. Not available on tvOS (subscriptions are managed in Settings > Accounts) or watchOS. /// - SeeAlso: https://developer.apple.com/documentation/storekit/appstore/showmanagesubscriptions(in:) /// - /// See: https://www.openiap.dev/docs/apis/deep-link-to-subscriptions + /// See: https://openiap.dev/docs/apis/deep-link-to-subscriptions public func deepLinkToSubscriptions(_ options: DeepLinkOptions?) async throws -> Void { try await ensureConnection() // tvOS: AppStore.showManageSubscriptions not available on tvOS (subscriptions managed in Settings > Accounts) @@ -987,7 +1009,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Get subscription status objects from StoreKit 2. - /// See: https://www.openiap.dev/docs/apis/ios/subscription-status-ios + /// See: https://openiap.dev/docs/apis/ios/subscription-status-ios public func subscriptionStatusIOS(sku: String) async throws -> [SubscriptionStatusIOS] { try await ensureConnection() let product = try await storeProduct(for: sku) @@ -1025,7 +1047,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Get the user's current entitlement for a product. - /// See: https://www.openiap.dev/docs/apis/ios/current-entitlement-ios + /// See: https://openiap.dev/docs/apis/ios/current-entitlement-ios public func currentEntitlementIOS(sku: String) async throws -> PurchaseIOS? { try await ensureConnection() let product = try await storeProduct(for: sku) @@ -1041,7 +1063,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Get the latest verified transaction for a product. - /// See: https://www.openiap.dev/docs/apis/ios/latest-transaction-ios + /// See: https://openiap.dev/docs/apis/ios/latest-transaction-ios public func latestTransactionIOS(sku: String) async throws -> PurchaseIOS? { try await ensureConnection() let product = try await storeProduct(for: sku) @@ -1062,7 +1084,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { /// - Note: Available on iOS 15.0+, iPadOS 15.0+, Mac Catalyst 15.0+, macOS 12.0+, visionOS 1.0+. Not available on tvOS or watchOS. /// - SeeAlso: https://developer.apple.com/documentation/storekit/transaction/3803220-beginrefundrequest /// - /// See: https://www.openiap.dev/docs/apis/ios/begin-refund-request-ios + /// See: https://openiap.dev/docs/apis/ios/begin-refund-request-ios public func beginRefundRequestIOS(sku: String) async throws -> String? { try await ensureConnection() // tvOS: Transaction.beginRefundRequest not available on tvOS @@ -1110,7 +1132,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { /// Check if the user is eligible for an introductory offer for a subscription group /// - SeeAlso: https://developer.apple.com/documentation/storekit/product/subscriptioninfo/iseligibleforintrooffer(for:) /// - /// See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios + /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios public func isEligibleForIntroOfferIOS(groupID: String) async throws -> Bool { try await ensureConnection() return await StoreKit.Product.SubscriptionInfo.isEligibleForIntroOffer(for: groupID) @@ -1119,7 +1141,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { /// Sync the user's in-app purchases with the App Store /// - SeeAlso: https://developer.apple.com/documentation/storekit/appstore/sync() /// - /// See: https://www.openiap.dev/docs/apis/ios/sync-ios + /// See: https://openiap.dev/docs/apis/ios/sync-ios public func syncIOS() async throws -> Bool { try await ensureConnection() do { @@ -1134,7 +1156,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { /// - Note: Only available on iOS 14.0+ and Mac Catalyst. Not available on tvOS, macOS, or watchOS /// - SeeAlso: https://developer.apple.com/documentation/storekit/skpaymentqueue/3566726-presentcoderedemptionsheet /// - /// See: https://www.openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios public func presentCodeRedemptionSheetIOS() async throws -> Bool { try await ensureConnection() // presentCodeRedemptionSheet is only available on iOS, not tvOS/watchOS/macOS @@ -1149,7 +1171,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Present the manage-subscriptions sheet. - /// See: https://www.openiap.dev/docs/apis/ios/show-manage-subscriptions-ios + /// See: https://openiap.dev/docs/apis/ios/show-manage-subscriptions-ios public func showManageSubscriptionsIOS() async throws -> [PurchaseIOS] { try await deepLinkToSubscriptions(nil) return [] @@ -1158,7 +1180,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { // MARK: - External Purchase (iOS 17.4+, macOS 14.4+, tvOS 17.4+, visionOS 1.1+) /// Check eligibility for the external purchase notice sheet (iOS 17.4+). - /// See: https://www.openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios + /// See: https://openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios public func canPresentExternalPurchaseNoticeIOS() async throws -> Bool { try await ensureConnection() // iOS 17.4+, macOS 14.4+, tvOS 17.4+, watchOS 10.4+, visionOS 1.1+: ExternalPurchase.canPresent @@ -1171,7 +1193,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Present the external purchase notice sheet (iOS 17.4+). - /// See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios + /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios public func presentExternalPurchaseNoticeSheetIOS() async throws -> ExternalPurchaseNoticeResultIOS { try await ensureConnection() // iOS 17.4+, macOS 14.4+, tvOS 17.4+, watchOS 10.4+, visionOS 1.1+: ExternalPurchase.presentNoticeSheet @@ -1235,7 +1257,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Present an external purchase link, StoreKit External (iOS 16+). - /// See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-link-ios + /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-link-ios public func presentExternalPurchaseLinkIOS(_ url: String) async throws -> ExternalPurchaseLinkResultIOS { try await ensureConnection() // UIApplication.open is available on iOS/tvOS/visionOS but not watchOS/macOS @@ -1269,7 +1291,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { // MARK: - ExternalPurchaseCustomLink (iOS 18.1+) /// Check eligibility for the custom-link variant of external purchase (iOS 18.1+). - /// See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios + /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios public func isEligibleForExternalPurchaseCustomLinkIOS() async throws -> Bool { try await ensureConnection() // iOS 18.1+: ExternalPurchaseCustomLink.isEligible @@ -1282,7 +1304,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Fetch a token for Apple's External Purchase Server reporting API (iOS 18.1+). - /// See: https://www.openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios + /// See: https://openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios public func getExternalPurchaseCustomLinkTokenIOS( _ tokenType: ExternalPurchaseCustomLinkTokenTypeIOS ) async throws -> ExternalPurchaseCustomLinkTokenResultIOS { @@ -1324,7 +1346,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Present the disclosure sheet required before linking out via ExternalPurchaseCustomLink (iOS 18.1+). - /// See: https://www.openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios + /// See: https://openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios public func showExternalPurchaseCustomLinkNoticeIOS( _ noticeType: ExternalPurchaseCustomLinkNoticeTypeIOS ) async throws -> ExternalPurchaseCustomLinkNoticeResultIOS { diff --git a/packages/apple/Sources/OpenIapProtocol.swift b/packages/apple/Sources/OpenIapProtocol.swift index 1a67292c..4d8a0768 100644 --- a/packages/apple/Sources/OpenIapProtocol.swift +++ b/packages/apple/Sources/OpenIapProtocol.swift @@ -91,6 +91,16 @@ public protocol OpenIapModuleProtocol { func presentCodeRedemptionSheetIOS() async throws -> Bool func showManageSubscriptionsIOS() async throws -> [PurchaseIOS] func deepLinkToSubscriptions(_ options: DeepLinkOptions?) async throws -> Void + func canPresentExternalPurchaseNoticeIOS() async throws -> Bool + func presentExternalPurchaseNoticeSheetIOS() async throws -> ExternalPurchaseNoticeResultIOS + func presentExternalPurchaseLinkIOS(_ url: String) async throws -> ExternalPurchaseLinkResultIOS + func isEligibleForExternalPurchaseCustomLinkIOS() async throws -> Bool + func getExternalPurchaseCustomLinkTokenIOS( + _ tokenType: ExternalPurchaseCustomLinkTokenTypeIOS + ) async throws -> ExternalPurchaseCustomLinkTokenResultIOS + func showExternalPurchaseCustomLinkNoticeIOS( + _ noticeType: ExternalPurchaseCustomLinkNoticeTypeIOS + ) async throws -> ExternalPurchaseCustomLinkNoticeResultIOS // Event Listeners func purchaseUpdatedListener( @@ -131,4 +141,50 @@ public extension OpenIapModuleProtocol { func validateReceipt(_ props: VerifyPurchaseProps) async throws -> VerifyPurchaseResult { try await verifyPurchase(props) } + + func canPresentExternalPurchaseNoticeIOS() async throws -> Bool { + throw PurchaseError( + code: .featureNotSupported, + message: "canPresentExternalPurchaseNoticeIOS not supported" + ) + } + + func presentExternalPurchaseNoticeSheetIOS() async throws -> ExternalPurchaseNoticeResultIOS { + throw PurchaseError( + code: .featureNotSupported, + message: "presentExternalPurchaseNoticeSheetIOS not supported" + ) + } + + func presentExternalPurchaseLinkIOS(_ url: String) async throws -> ExternalPurchaseLinkResultIOS { + throw PurchaseError( + code: .featureNotSupported, + message: "presentExternalPurchaseLinkIOS not supported" + ) + } + + func isEligibleForExternalPurchaseCustomLinkIOS() async throws -> Bool { + throw PurchaseError( + code: .featureNotSupported, + message: "isEligibleForExternalPurchaseCustomLinkIOS not supported" + ) + } + + func getExternalPurchaseCustomLinkTokenIOS( + _ tokenType: ExternalPurchaseCustomLinkTokenTypeIOS + ) async throws -> ExternalPurchaseCustomLinkTokenResultIOS { + throw PurchaseError( + code: .featureNotSupported, + message: "getExternalPurchaseCustomLinkTokenIOS not supported" + ) + } + + func showExternalPurchaseCustomLinkNoticeIOS( + _ noticeType: ExternalPurchaseCustomLinkNoticeTypeIOS + ) async throws -> ExternalPurchaseCustomLinkNoticeResultIOS { + throw PurchaseError( + code: .featureNotSupported, + message: "showExternalPurchaseCustomLinkNoticeIOS not supported" + ) + } } diff --git a/packages/apple/Sources/OpenIapStore.swift b/packages/apple/Sources/OpenIapStore.swift index c7229a69..f710f5e5 100644 --- a/packages/apple/Sources/OpenIapStore.swift +++ b/packages/apple/Sources/OpenIapStore.swift @@ -55,11 +55,15 @@ public final class OpenIapStore: ObservableObject { setupListeners() } - deinit { listenerTokens.removeAll() } + deinit { + for token in listenerTokens { module.removeListener(token) } + } // MARK: - Listener Management private func setupListeners() { + guard listenerTokens.isEmpty else { return } + let purchaseUpdate = module.purchaseUpdatedListener({ [weak self] purchase in Task { @MainActor in self?.handlePurchaseUpdate(purchase) } }, options: nil) @@ -89,6 +93,7 @@ public final class OpenIapStore: ObservableObject { status.loadings.initConnection = true defer { status.loadings.initConnection = false } isConnected = try await module.initConnection() + setupListeners() } public func endConnection() async throws { @@ -472,6 +477,34 @@ public final class OpenIapStore: ObservableObject { } #endif // !os(tvOS) + public func canPresentExternalPurchaseNoticeIOS() async throws -> Bool { + try await module.canPresentExternalPurchaseNoticeIOS() + } + + public func presentExternalPurchaseNoticeSheetIOS() async throws -> ExternalPurchaseNoticeResultIOS { + try await module.presentExternalPurchaseNoticeSheetIOS() + } + + public func presentExternalPurchaseLinkIOS(_ url: String) async throws -> ExternalPurchaseLinkResultIOS { + try await module.presentExternalPurchaseLinkIOS(url) + } + + public func isEligibleForExternalPurchaseCustomLinkIOS() async throws -> Bool { + try await module.isEligibleForExternalPurchaseCustomLinkIOS() + } + + public func getExternalPurchaseCustomLinkTokenIOS( + _ tokenType: ExternalPurchaseCustomLinkTokenTypeIOS + ) async throws -> ExternalPurchaseCustomLinkTokenResultIOS { + try await module.getExternalPurchaseCustomLinkTokenIOS(tokenType) + } + + public func showExternalPurchaseCustomLinkNoticeIOS( + _ noticeType: ExternalPurchaseCustomLinkNoticeTypeIOS + ) async throws -> ExternalPurchaseCustomLinkNoticeResultIOS { + try await module.showExternalPurchaseCustomLinkNoticeIOS(noticeType) + } + public func clearTransactionIOS() async throws { _ = try await module.clearTransactionIOS() } diff --git a/packages/apple/Sources/OpenIapVersion.swift b/packages/apple/Sources/OpenIapVersion.swift index a4e1686d..e8355105 100644 --- a/packages/apple/Sources/OpenIapVersion.swift +++ b/packages/apple/Sources/OpenIapVersion.swift @@ -1,15 +1,65 @@ import Foundation +private final class OpenIapVersionBundleToken {} + /// OpenIAP version management public struct OpenIapVersion { /// Current OpenIAP Apple SDK version - /// This version is managed in monorepo root versions.json - public static let current: String = "1.2.23" + public static var current: String { + version(for: "apple") + } + + /// Current OpenIAP specification version + public static var specVersion: String { + version(for: "spec") + } /// OpenIAP GraphQL version for reference - /// This version is managed in monorepo root versions.json - public static let gqlVersion: String = "1.2.2" + @available(*, deprecated, renamed: "specVersion") + public static var gqlVersion: String { + specVersion + } + + private static func version(for key: String) -> String { + let versionURL: URL? + + #if SWIFT_PACKAGE + versionURL = Bundle.module.url(forResource: "openiap-versions", withExtension: "json") + #else + versionURL = cocoaPodsVersionURL() + #endif + guard + let url = versionURL, + let data = try? Data(contentsOf: url), + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let version = json[key] as? String, + !version.isEmpty + else { + fatalError("OpenIAP: missing \(key) version in openiap-versions.json") + } + return version + } + + private static func cocoaPodsVersionURL() -> URL? { + let bundles = [Bundle(for: OpenIapVersionBundleToken.self), Bundle.main] + Bundle.allBundles + + for bundle in bundles { + if let url = bundle.url(forResource: "openiap-versions", withExtension: "json") { + return url + } + + if + let bundleURL = bundle.url(forResource: "OpenIAP", withExtension: "bundle"), + let resourceBundle = Bundle(url: bundleURL), + let url = resourceBundle.url(forResource: "openiap-versions", withExtension: "json") + { + return url + } + } + + return nil + } } // MARK: - Version Info @@ -22,7 +72,8 @@ public enum OpenIapVersionInfo { } /// OpenIAP GraphQL version for reference + @available(*, deprecated, renamed: "specVersion") public static var gqlVersion: String { - OpenIapVersion.gqlVersion + OpenIapVersion.specVersion } -} \ No newline at end of file +} diff --git a/packages/apple/openiap.podspec b/packages/apple/openiap.podspec index b855c173..89044445 100644 --- a/packages/apple/openiap.podspec +++ b/packages/apple/openiap.podspec @@ -38,6 +38,11 @@ Pod::Spec.new do |s| # When podspec is in repo root (git distribution), use 'packages/apple/Sources/**/*.swift' sources_dir = File.join(File.dirname(__FILE__), 'Sources') s.source_files = File.directory?(sources_dir) ? 'Sources/**/*.swift' : 'packages/apple/Sources/**/*.swift' + s.resource_bundles = { + 'OpenIAP' => [ + File.directory?(sources_dir) ? 'Sources/openiap-versions.json' : 'packages/apple/Sources/openiap-versions.json' + ] + } s.frameworks = 'StoreKit' s.requires_arc = true diff --git a/packages/apple/package.json b/packages/apple/package.json index 6c8f12f3..7b209ec0 100644 --- a/packages/apple/package.json +++ b/packages/apple/package.json @@ -1,6 +1,6 @@ { "name": "@hyodotdev/openiap-ios", - "version": "1.2.23", + "version": "2.1.9", "private": true, "description": "OpenIAP iOS/Swift implementation", "scripts": { @@ -12,5 +12,5 @@ "dependencies": { "@hyodotdev/openiap-gql": "workspace:*" }, - "packageManager": "bun@1.1.0" + "packageManager": "bun@1.3.13" } diff --git a/packages/apple/scripts/build-xcframework.sh b/packages/apple/scripts/build-xcframework.sh index 92f84f74..5acbd314 100755 --- a/packages/apple/scripts/build-xcframework.sh +++ b/packages/apple/scripts/build-xcframework.sh @@ -13,15 +13,42 @@ BUILD_DIR="${PACKAGE_DIR}/.build/xcframework" DERIVED="${BUILD_DIR}/derived" ARCHIVES="${BUILD_DIR}/archives" OUT="${BUILD_DIR}/OpenIAP.xcframework" +VERSIONS_FILE="${PACKAGE_DIR}/Sources/openiap-versions.json" +if [[ ! -f "${VERSIONS_FILE}" ]]; then + VERSIONS_FILE="${PACKAGE_DIR}/../../openiap-versions.json" +fi if [[ ! -d "${WRAPPER_DIR}" ]] || [[ ! -f "${WRAPPER_DIR}/project.yml" ]]; then echo "error: wrapper project not found at ${WRAPPER_DIR}" exit 1 fi +if [[ ! -f "${VERSIONS_FILE}" ]]; then + echo "error: openiap-versions.json not found at ${VERSIONS_FILE}" + exit 1 +fi + +read_openiap_version() { + python3 - "$VERSIONS_FILE" "$1" <<'PY' +import json +import sys + +path, key = sys.argv[1], sys.argv[2] +with open(path, encoding="utf-8") as file: + value = json.load(file).get(key) + +if not isinstance(value, str) or not value.strip(): + raise SystemExit(f"missing {key} in {path}") + +print(value.strip()) +PY +} + +APPLE_VERSION="$(read_openiap_version apple)" + # Regenerate the wrapper Xcode project (xcodegen) so source-file changes are picked up. if ! command -v xcodegen >/dev/null 2>&1; then - echo "error: xcodegen not installed (brew install xcodegen)" + echo "error: xcodegen not installed (run scripts/install-xcodegen.sh )" exit 1 fi @@ -43,6 +70,7 @@ archive() { -archivePath "${archive_path}" \ -derivedDataPath "${DERIVED}" \ -configuration Release \ + OPENIAP_MARKETING_VERSION="${APPLE_VERSION}" \ SKIP_INSTALL=NO \ BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ -quiet diff --git a/packages/apple/scripts/bump-version.sh b/packages/apple/scripts/bump-version.sh index 53c68370..557563fe 100755 --- a/packages/apple/scripts/bump-version.sh +++ b/packages/apple/scripts/bump-version.sh @@ -1,17 +1,32 @@ -#!/bin/bash +#!/usr/bin/env bash # Usage: ./scripts/bump-version.sh [major|minor|patch|x.x.x] -set -e +set -euo pipefail -VERSIONS_FILE="openiap-versions.json" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" +VERSIONS_FILE="${REPO_ROOT}/openiap-versions.json" # Get current version from openiap-versions.json -if [ -f "${VERSIONS_FILE}" ]; then +if [[ -f "${VERSIONS_FILE}" ]]; then if command -v jq &> /dev/null; then - CURRENT_VERSION=$(jq -r '.apple' "${VERSIONS_FILE}") + CURRENT_VERSION=$(jq -er '.apple | select(type == "string" and length > 0)' "${VERSIONS_FILE}") elif command -v python3 &> /dev/null; then - CURRENT_VERSION=$(python3 -c "import json; print(json.load(open('${VERSIONS_FILE}'))['apple'])") + CURRENT_VERSION=$(python3 - "${VERSIONS_FILE}" <<'PY' +import json +import sys + +path = sys.argv[1] +with open(path, encoding="utf-8") as file: + value = json.load(file).get("apple") + +if not isinstance(value, str) or not value.strip(): + raise SystemExit(f"missing apple in {path}") + +print(value.strip()) +PY +) else echo "❌ Error: jq or python3 is required to read openiap-versions.json" exit 1 @@ -30,7 +45,7 @@ MINOR="${VERSION_PARTS[1]}" PATCH="${VERSION_PARTS[2]}" # Determine new version -if [ -z "$1" ]; then +if [[ -z "${1:-}" ]]; then echo "Usage: $0 [major|minor|patch|x.x.x]" exit 1 fi @@ -54,64 +69,54 @@ esac echo "New version: $NEW_VERSION" # Update openiap-versions.json -if [ -f "openiap-versions.json" ]; then - if command -v jq &> /dev/null; then - # Use jq to update JSON - jq --arg version "$NEW_VERSION" '.apple = $version' openiap-versions.json > openiap-versions.json.tmp && \ - mv openiap-versions.json.tmp openiap-versions.json - echo "✅ Updated openiap-versions.json" - elif command -v python3 &> /dev/null; then - # Use python3 as fallback - python3 -c " +if command -v jq &> /dev/null; then + tmp_file="${VERSIONS_FILE}.tmp" + jq --arg version "$NEW_VERSION" '.apple = $version' "$VERSIONS_FILE" > "$tmp_file" + mv "$tmp_file" "$VERSIONS_FILE" + echo "✅ Updated openiap-versions.json" +elif command -v python3 &> /dev/null; then + VERSION="$NEW_VERSION" VERSIONS_FILE="$VERSIONS_FILE" python3 - <<'PY' import json -with open('openiap-versions.json', 'r') as f: +import os + +versions_file = os.environ["VERSIONS_FILE"] +with open(versions_file, 'r', encoding='utf-8') as f: data = json.load(f) -data['apple'] = '$NEW_VERSION' -with open('openiap-versions.json', 'w') as f: +data['apple'] = os.environ["VERSION"] +with open(versions_file, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2) f.write('\n') -" - echo "✅ Updated openiap-versions.json (using python3)" - else - echo "⚠️ Warning: jq and python3 not available. Skipping openiap-versions.json update" - fi -fi - -# Update OpenIapVersion.swift fallback version -if [ -f "Sources/OpenIapVersion.swift" ]; then - sed -i '' "s/return \"[0-9.]*\"/return \"$NEW_VERSION\"/" Sources/OpenIapVersion.swift - echo "✅ Updated OpenIapVersion.swift fallback" +PY + echo "✅ Updated openiap-versions.json (using python3)" +else + echo "❌ Error: jq or python3 is required to update openiap-versions.json" + exit 1 fi -# Note: openiap.podspec now reads version from openiap-versions.json automatically +"$REPO_ROOT/scripts/sync-versions.sh" -# Update README.md - CocoaPods installation -sed -i '' "s/pod 'openiap', '~> [0-9.]*'/pod 'openiap', '~> $NEW_VERSION'/" README.md - -# Update README.md - Swift Package Manager -sed -i '' "s/.package(url: \"https:\/\/github.com\/hyodotdev\/openiap-apple.git\", from: \"[0-9.]*\")/.package(url: \"https:\/\/github.com\/hyodotdev\/openiap-apple.git\", from: \"$NEW_VERSION\")/" README.md +# openiap.podspec reads the Apple version from openiap-versions.json. # Commit changes -git add README.md openiap-versions.json Sources/OpenIapVersion.swift -git commit -m "chore: bump version to $NEW_VERSION" +cd "$REPO_ROOT" +git add openiap-versions.json packages/*/openiap-versions.json +git add packages/gql/package.json packages/docs/package.json packages/google/package.json packages/apple/package.json +git commit -m "chore(apple): bump version to $NEW_VERSION" # Push commits -git push origin main +git pull --rebase origin main +git push origin HEAD:main # Create and push tag (with check) if git rev-parse "refs/tags/$NEW_VERSION" >/dev/null 2>&1; then - echo "⚠️ Tag $NEW_VERSION already exists locally, deleting and recreating..." - git tag -d "$NEW_VERSION" -fi - -git tag "$NEW_VERSION" - -# Try to push tag, ignore error if already exists -if ! git push origin "$NEW_VERSION" 2>/dev/null; then - echo "ℹ️ Tag $NEW_VERSION already exists on remote (probably from CocoaPods release)" + echo "ℹ️ Tag $NEW_VERSION already exists locally. Reusing existing tag." +elif git ls-remote --exit-code --tags origin "refs/tags/$NEW_VERSION" >/dev/null 2>&1; then + echo "ℹ️ Tag $NEW_VERSION already exists on remote. Reusing existing tag." else + git tag "$NEW_VERSION" + git push origin "$NEW_VERSION" echo "✅ Tag $NEW_VERSION pushed successfully" fi echo "✅ Version bumped to $NEW_VERSION and pushed!" -echo "📦 Ready to create a GitHub Release with tag $NEW_VERSION" \ No newline at end of file +echo "📦 Ready to create a GitHub Release with tag $NEW_VERSION" diff --git a/packages/apple/wrapper/project.yml b/packages/apple/wrapper/project.yml index f27d7df8..35b71ad0 100644 --- a/packages/apple/wrapper/project.yml +++ b/packages/apple/wrapper/project.yml @@ -18,8 +18,9 @@ settings: DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER: NO GENERATE_INFOPLIST_FILE: YES PRODUCT_BUNDLE_IDENTIFIER: dev.hyo.openiap.OpenIAP - MARKETING_VERSION: "1.2.5" + MARKETING_VERSION: "$(OPENIAP_MARKETING_VERSION)" CURRENT_PROJECT_VERSION: "1" + SUPPORTED_PLATFORMS: "iphoneos iphonesimulator macosx" targets: OpenIAP: type: framework @@ -32,6 +33,8 @@ targets: base: PRODUCT_NAME: OpenIAP PRODUCT_BUNDLE_IDENTIFIER: dev.hyo.openiap.OpenIAP + SUPPORTED_PLATFORMS: "iphoneos iphonesimulator macosx" + TARGETED_DEVICE_FAMILY: "1,2,6" SWIFT_INSTALL_OBJC_HEADER: YES SWIFT_OBJC_INTERFACE_HEADER_NAME: OpenIAP-Swift.h SWIFT_EMIT_MODULE_INTERFACE: YES diff --git a/packages/docs/CONVENTION.md b/packages/docs/CONVENTION.md index 1be686ab..5cd74f03 100644 --- a/packages/docs/CONVENTION.md +++ b/packages/docs/CONVENTION.md @@ -22,3 +22,7 @@ - Home, `/languages`, setup pages, sidebars, and sponsor lists should derive framework entries from `LIBRARIES`; add metadata fields there instead of duplicating local arrays. +- Install commands that include package versions should derive from package + metadata helpers (for example `FLUTTER_PACKAGE`, `KMP_PACKAGE`, and + `MAUI_PACKAGE` in `src/lib/versioning.ts`), not inline version literals in + page components. diff --git a/packages/docs/deploy.sh b/packages/docs/deploy.sh index 30a1ee6d..bde4d075 100755 --- a/packages/docs/deploy.sh +++ b/packages/docs/deploy.sh @@ -3,7 +3,7 @@ # OpenIAP.dev Vercel Deployment Script # This script handles local deployment to Vercel for organization repositories -set -e +set -euo pipefail echo "🚀 Starting OpenIAP.dev deployment to Vercel..." @@ -13,11 +13,12 @@ GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color +VERCEL_CLI_VERSION="54.0.0" # Check if Vercel CLI is installed if ! command -v vercel &> /dev/null; then - echo -e "${YELLOW}⚠️ Vercel CLI not found. Installing globally...${NC}" - npm install -g vercel + echo -e "${YELLOW}⚠️ Vercel CLI not found. Installing v${VERCEL_CLI_VERSION} globally...${NC}" + npm install -g "vercel@$VERCEL_CLI_VERSION" echo -e "${GREEN}✅ Vercel CLI installed successfully${NC}" fi @@ -30,14 +31,12 @@ fi echo -e "${BLUE}📦 Building project...${NC}" # Run type checking and build -bun run typecheck -if [ $? -ne 0 ]; then +if ! bun run typecheck; then echo -e "${RED}❌ TypeScript errors found. Please fix them before deploying.${NC}" exit 1 fi -bun run build -if [ $? -ne 0 ]; then +if ! bun run build; then echo -e "${RED}❌ Build failed. Please check the errors above.${NC}" exit 1 fi @@ -61,4 +60,4 @@ if [ $? -eq 0 ]; then else echo -e "${RED}❌ Deployment failed. Check the errors above.${NC}" exit 1 -fi \ No newline at end of file +fi diff --git a/packages/docs/package.json b/packages/docs/package.json index 749f7c67..35fc9e63 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -1,7 +1,7 @@ { "name": "@hyodotdev/openiap-docs", "private": true, - "version": "2.0.0", + "version": "2.0.2", "type": "module", "scripts": { "dev": "bunx vite", @@ -28,16 +28,16 @@ "react-toastify": "^11.0.2" }, "devDependencies": { - "@eslint/js": "^9.33.0", + "@eslint/js": "^9.39.4", "@preact/signals-react-transform": "^0.5.2", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@typescript-eslint/eslint-plugin": "^8.39.1", "@typescript-eslint/parser": "^8.39.1", - "@vitejs/plugin-react": "^4.3.1", + "@vitejs/plugin-react": "^5.2.0", "babel-plugin-react-compiler": "^19.1.0-rc.2", "baseline-browser-mapping": "^2.10.22", - "eslint": "^9.21.0", + "eslint": "^9.39.4", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.7", "globals": "^16.3.0", @@ -45,15 +45,14 @@ "lint-staged": "^16.1.5", "lucide-react": "^0.539.0", "prettier": "^3.6.2", - "sharp-cli": "^5.2.0", "typescript": "^5.2.2", "typescript-eslint": "^8.39.1", - "vite": "^5.3.1" + "vite": "^6.4.2" }, "lint-staged": { "*.{ts,tsx,js,jsx,css,json}": [ "prettier --write" ] }, - "packageManager": "bun@1.1.0" + "packageManager": "bun@1.3.13" } diff --git a/packages/docs/public/llms-full.txt b/packages/docs/public/llms-full.txt index 4f628274..561c961e 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-08T10:48:46.777Z +> Generated: 2026-05-16T12:59:43.331Z ## Table of Contents 1. Installation @@ -30,44 +30,47 @@ cd ios && pod install ### Swift (iOS/macOS) ```swift // Swift Package Manager -.package(url: "https://github.com/hyodotdev/openiap.git", from: "1.0.0") +.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.1.9") // CocoaPods -pod 'openiap', '~> 1.0.0' +pod 'openiap', '~> 2.1.9' ``` ### Kotlin (Android) ```kotlin // Gradle (build.gradle.kts) -implementation("io.github.hyochan.openiap:openiap-google:1.0.0") +implementation("io.github.hyochan.openiap:openiap-google:2.1.5") // For Meta Horizon OS -implementation("io.github.hyochan.openiap:openiap-google-horizon:1.0.0") +implementation("io.github.hyochan.openiap:openiap-google-horizon:2.1.5") ``` ### Flutter -```yaml -# pubspec.yaml -dependencies: - flutter_inapp_purchase: ^5.0.0 +```bash +flutter pub add flutter_inapp_purchase ``` ### Godot -Download `godot-iap` from the Godot Asset Library or GitHub Releases, extract -it to `addons/godot-iap/`, then enable the plugin in Project Settings. +Download `godot-iap-2.2.10.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.kmpiap:library:1.3.8") + implementation("io.github.hyochan:kmp-iap:2.2.8") } ``` +Use the latest version from Maven Central: +https://central.sonatype.com/artifact/io.github.hyochan/kmp-iap + ### .NET MAUI -```xml - +```bash +dotnet add package OpenIap.Maui ``` +Current NuGet package version: 1.0.4 + Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. --- @@ -103,7 +106,7 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. - Public surface: snake_case functions and Godot signals matching OpenIAP. ### kmp-iap -- Package: `io.github.hyochan.kmpiap:library`. +- Package: `io.github.hyochan:kmp-iap`. - Implementation: Kotlin Multiplatform common API with Flow-based events, Android implementation, and iOS cinterop through the OpenIAP ObjC facade. - Public surface: `KmpIAP` / shared instance resolver methods and flows. @@ -601,7 +604,7 @@ Google Play Billing Library enables in-app purchases and subscriptions on Androi | 8.2.1 | 2025-12-15 | Bug fix for `isBillingProgramAvailableAsync()` and `createBillingProgramReportingDetailsAsync()` | | 8.3 | 2025-12-23 | External Payments program (Japan only), developer billing options | -**Current Version**: 8.3.0 (as of January 2026) +**Current Version**: 8.3.0 (as of April 2026) ## Core Classes diff --git a/packages/docs/public/llms.txt b/packages/docs/public/llms.txt index 9b6b2381..45cfb2e2 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-08T10:48:46.778Z +> Generated: 2026-05-16T12:59:43.331Z ## Installation @@ -19,35 +19,36 @@ npm install react-native-iap ### Native ```swift // Swift Package Manager -.package(url: "https://github.com/hyodotdev/openiap.git", from: "1.0.0") +.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.1.9") ``` ```kotlin // Gradle -implementation("io.github.hyochan.openiap:openiap-google:1.0.0") +implementation("io.github.hyochan.openiap:openiap-google:2.1.5") ``` -```yaml +```bash # Flutter -dependencies: - flutter_inapp_purchase: ^5.0.0 +flutter pub add flutter_inapp_purchase ``` ```gdscript # Godot -# Install godot-iap to addons/godot-iap and enable the plugin +# Install godot-iap 2.2.10 to addons/godot-iap and enable the plugin ``` ```kotlin // Kotlin Multiplatform -implementation("io.github.hyochan.kmpiap:library:1.3.8") +implementation("io.github.hyochan:kmp-iap:2.2.8") ``` -```xml - - +```bash +# .NET MAUI +dotnet add package OpenIap.Maui ``` +Current NuGet package version: 1.0.4 + ## Framework Libraries - `expo-iap`: Expo Modules wrapper, same OpenIAP API as React Native. diff --git a/packages/docs/src/components/FeatureCode.tsx b/packages/docs/src/components/FeatureCode.tsx deleted file mode 100644 index cccac43e..00000000 --- a/packages/docs/src/components/FeatureCode.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { useState } from 'react'; - -interface FeatureCodeProps { - code: string; -} - -function FeatureCode({ code }: FeatureCodeProps) { - const [copied, setCopied] = useState(false); - - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(code); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (err) { - console.error('Failed to copy:', err); - } - }; - - // Simple syntax highlighting for JavaScript/TypeScript - const highlightCode = (text: string) => { - return ( - text - // Comments - .replace(/(\/\/[^\n]*)/g, '$1') - // Strings - .replace(/('[^']*')/g, '$1') - // Keywords - .replace( - /\b(import|from|await|async|const|let|var|function)\b/g, - '$1' - ) - // Function names - .replace( - /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?=\()/g, - '$1' - ) - // Properties - .replace( - /\.([a-zA-Z_$][a-zA-Z0-9_$]*)/g, - '.$1' - ) - ); - }; - - return ( -
- -
-        
-      
-
- ); -} - -export default FeatureCode; diff --git a/packages/docs/src/lib/images.ts b/packages/docs/src/lib/images.ts index 7cc2f242..5fbe0c0b 100644 --- a/packages/docs/src/lib/images.ts +++ b/packages/docs/src/lib/images.ts @@ -5,6 +5,15 @@ // Keep framework listings in pages derived from LIBRARIES to avoid drift. // ============================================================================= +import { + EXPO_PACKAGE, + FLUTTER_PACKAGE, + GODOT_PACKAGE, + KMP_PACKAGE, + MAUI_PACKAGE, + REACT_NATIVE_PACKAGE, +} from './versioning'; + export const LIBRARY_IMAGES = { 'openiap-apple': '/frameworks/apple.svg', 'openiap-google': '/frameworks/google.svg', @@ -61,8 +70,8 @@ export const LIBRARIES: LibraryInfo[] = [ 'React Native & Expo implementation of OpenIAP specification (Expo Modules)', setupDescription: 'Expo SDK projects via Expo Modules. Same API surface as react-native-iap, including the `useIAP` hook, with managed-workflow-friendly install. Recommended for any Expo app.', - installCommand: 'npm install expo-iap', - documentationUrl: 'https://hyochan.github.io/expo-iap', + installCommand: EXPO_PACKAGE.installCommand, + documentationUrl: 'https://openiap.dev/docs/setup/expo', url: 'https://github.com/hyodotdev/openiap/tree/main/libraries/expo-iap', image: LIBRARY_IMAGES['expo-iap'], imageAlt: 'Expo IAP', @@ -79,8 +88,8 @@ export const LIBRARIES: LibraryInfo[] = [ 'React Native & Expo implementation of OpenIAP specification (Nitro Modules)', setupDescription: 'Bare React Native CLI projects (RN 0.79+). Built on Nitro Modules with the `useIAP` hook, error normalization, and full StoreKit 2 / Play Billing 8 coverage.', - installCommand: 'npm install react-native-iap', - documentationUrl: 'https://hyochan.github.io/react-native-iap', + installCommand: REACT_NATIVE_PACKAGE.installCommand, + documentationUrl: 'https://openiap.dev/docs/setup/react-native', url: 'https://github.com/hyodotdev/openiap/tree/main/libraries/react-native-iap', image: LIBRARY_IMAGES['react-native-iap'], imageAlt: 'React Native IAP', @@ -96,8 +105,8 @@ export const LIBRARIES: LibraryInfo[] = [ languagesDescription: 'Flutter implementation of OpenIAP specification', setupDescription: 'Flutter apps via the `flutter_inapp_purchase` package. Generated `types.dart`, sealed-class results, and a Stream-based event API that mirrors the OpenIAP schema.', - installCommand: 'flutter pub add flutter_inapp_purchase', - documentationUrl: 'https://hyochan.github.io/flutter_inapp_purchase', + installCommand: FLUTTER_PACKAGE.installCommand, + documentationUrl: 'https://openiap.dev/docs/setup/flutter', url: 'https://github.com/hyodotdev/openiap/tree/main/libraries/flutter_inapp_purchase', image: LIBRARY_IMAGES['flutter_inapp_purchase'], imageAlt: 'Flutter IAP', @@ -114,8 +123,8 @@ export const LIBRARIES: LibraryInfo[] = [ 'Kotlin Multiplatform implementation of OpenIAP specification', setupDescription: 'KMP / Compose Multiplatform via the `kmp-iap` library. Flow-based API on top of OpenIAP, with CocoaPods integration for iOS targets and shared business logic across platforms.', - installCommand: 'implementation("io.github.hyochan:kmp-iap:1.0.0-rc.6")', - documentationUrl: 'https://hyochan.github.io/kmp-iap', + installCommand: KMP_PACKAGE.installCommand, + documentationUrl: 'https://openiap.dev/docs/setup/kmp', url: 'https://github.com/hyodotdev/openiap/tree/main/libraries/kmp-iap', image: LIBRARY_IMAGES['kmp-iap'], imageAlt: 'KMP IAP', @@ -132,7 +141,7 @@ export const LIBRARIES: LibraryInfo[] = [ '.NET MAUI / C# implementation of OpenIAP specification', setupDescription: '.NET MAUI / C# 12 via the `maui-iap` library (OpenIap.Maui on NuGet). Ships as one NuGet package with generated `Types.cs`, flattened Android AAR bindings, and StoreKit xcframework resources for iOS / macCatalyst.', - installCommand: 'dotnet add package OpenIap.Maui --version 1.0.1', + installCommand: MAUI_PACKAGE.installCommand, documentationUrl: '/docs/setup/maui', url: 'https://github.com/hyodotdev/openiap/tree/main/libraries/maui-iap', image: LIBRARY_IMAGES['maui-iap'], @@ -150,9 +159,8 @@ export const LIBRARIES: LibraryInfo[] = [ 'Godot implementation of OpenIAP specification (GDScript)', setupDescription: 'Godot 4.x via the `godot-iap` plugin (iOS GDExtension + Android AAR). Exposes the same OpenIAP function set so the same purchase flow can ship across mobile + console targets.', - releaseUrl: 'https://github.com/hyodotdev/openiap/releases', - documentationUrl: - 'https://github.com/hyodotdev/openiap/tree/main/libraries/godot-iap', + releaseUrl: GODOT_PACKAGE.releaseUrl, + documentationUrl: 'https://openiap.dev/docs/setup/godot', url: 'https://github.com/hyodotdev/openiap/tree/main/libraries/godot-iap', image: LIBRARY_IMAGES['godot-iap'], imageAlt: 'Godot IAP', diff --git a/packages/docs/src/lib/versioning.ts b/packages/docs/src/lib/versioning.ts index 25c397e8..950f64bb 100644 --- a/packages/docs/src/lib/versioning.ts +++ b/packages/docs/src/lib/versioning.ts @@ -1,21 +1,37 @@ import versionsFile from '../../openiap-versions.json?raw'; +import expoPackageFile from '../../../../libraries/expo-iap/package.json?raw'; +import reactNativePackageFile from '../../../../libraries/react-native-iap/package.json?raw'; +import flutterPubspecFile from '../../../../libraries/flutter_inapp_purchase/pubspec.yaml?raw'; +import godotPluginFile from '../../../../libraries/godot-iap/addons/godot-iap/plugin.cfg?raw'; +import kmpGradlePropertiesFile from '../../../../libraries/kmp-iap/gradle.properties?raw'; +import kmpVersionsCatalogFile from '../../../../libraries/kmp-iap/gradle/libs.versions.toml?raw'; +import mauiProjectFile from '../../../../libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj?raw'; +import googleOpenIapBuildFile from '../../../../packages/google/openiap/build.gradle.kts?raw'; -type VersionKey = 'spec'; +type VersionKey = 'spec' | 'google' | 'apple'; type VersionRecord = Record; -const REQUIRED_KEYS: readonly VersionKey[] = ['spec'] as const; +const REQUIRED_KEYS: readonly VersionKey[] = [ + 'spec', + 'google', + 'apple', +] as const; -function parseVersions(json: string): Record { +function parseJson(json: string, label: string): Record { try { return JSON.parse(json) as Record; } catch { throw new Error( - 'openiap-versions.json contains invalid JSON. Check the file for syntax errors.' + `${label} contains invalid JSON. Check the file for syntax errors.` ); } } +function parseVersions(json: string): Record { + return parseJson(json, 'openiap-versions.json'); +} + function ensureVersions(data: Record): VersionRecord { return REQUIRED_KEYS.reduce>((accumulator, key) => { const value = data[key]; @@ -29,7 +45,121 @@ function ensureVersions(data: Record): VersionRecord { }, {}) as VersionRecord; } +function readRequiredXmlValue( + xml: string, + tagName: string, + label: string +): string { + const match = xml.match(new RegExp(`<${tagName}>([^<]+)`)); + const value = match?.[1]?.trim(); + + if (!value) { + throw new Error(`${label} missing <${tagName}>`); + } + + return value; +} + +function readRequiredPackageJsonString( + json: string, + key: string, + label: string +): string { + const value = parseJson(json, label)[key]; + + if (typeof value !== 'string' || value.trim() === '') { + throw new Error(`${label} missing "${key}" string`); + } + + return value; +} + +function readRequiredMatchValue( + source: string, + pattern: RegExp, + label: string +): string { + const value = source.match(pattern)?.[1]?.trim(); + + if (!value) { + throw new Error(`${label} not found`); + } + + return value; +} + const parsedVersions = parseVersions(versionsFile); +const EXPO_PACKAGE_VERSION = readRequiredPackageJsonString( + expoPackageFile, + 'version', + 'expo-iap package.json' +); +const REACT_NATIVE_PACKAGE_VERSION = readRequiredPackageJsonString( + reactNativePackageFile, + 'version', + 'react-native-iap package.json' +); +const MAUI_PACKAGE_ID = readRequiredXmlValue( + mauiProjectFile, + 'PackageId', + 'OpenIap.Maui.csproj' +); +const MAUI_PACKAGE_VERSION = readRequiredXmlValue( + mauiProjectFile, + 'PackageVersion', + 'OpenIap.Maui.csproj' +); +const FLUTTER_PACKAGE_VERSION = flutterPubspecFile + .match(/^version:\s*(.+)$/m)?.[1] + ?.trim(); +const GODOT_PACKAGE_VERSION = godotPluginFile + .match(/^version="([^"]+)"$/m)?.[1] + ?.trim(); +const KMP_PACKAGE_VERSION = kmpGradlePropertiesFile + .match(/^libraryVersion=(.+)$/m)?.[1] + ?.trim(); +const GOOGLE_COMPILE_SDK = readRequiredMatchValue( + googleOpenIapBuildFile, + /compileSdk\s*=\s*(\d+)/, + 'packages/google openiap compileSdk' +); +const GOOGLE_MIN_SDK = readRequiredMatchValue( + googleOpenIapBuildFile, + /minSdk\s*=\s*(\d+)/, + 'packages/google openiap minSdk' +); +const GOOGLE_PLAY_BILLING_VERSION = readRequiredMatchValue( + googleOpenIapBuildFile, + /val\s+playBillingVersion\s*=\s*"([^"]+)"/, + 'packages/google Play Billing version' +); +const KMP_COMPILE_SDK = readRequiredMatchValue( + kmpVersionsCatalogFile, + /^android-compileSdk = "([^"]+)"/m, + 'kmp-iap android-compileSdk' +); +const KMP_MIN_SDK = readRequiredMatchValue( + kmpVersionsCatalogFile, + /^android-minSdk = "([^"]+)"/m, + 'kmp-iap android-minSdk' +); +const KMP_TARGET_SDK = readRequiredMatchValue( + kmpVersionsCatalogFile, + /^android-targetSdk = "([^"]+)"/m, + 'kmp-iap android-targetSdk' +); + +if (!FLUTTER_PACKAGE_VERSION) { + throw new Error('flutter_inapp_purchase pubspec.yaml missing version'); +} + +if (!GODOT_PACKAGE_VERSION) { + throw new Error('godot-iap plugin.cfg missing version'); +} + +if (!KMP_PACKAGE_VERSION) { + throw new Error('kmp-iap gradle.properties missing libraryVersion'); +} export const OPENIAP_VERSIONS = Object.freeze(ensureVersions(parsedVersions)); @@ -38,3 +168,67 @@ export const GQL_RELEASE = Object.freeze({ pageUrl: `https://github.com/hyodotdev/openiap/releases/tag/${OPENIAP_VERSIONS.spec}`, downloadPrefix: `https://github.com/hyodotdev/openiap/releases/download/${OPENIAP_VERSIONS.spec}/`, }); + +export const ANDROID_SDK = Object.freeze({ + minSdk: GOOGLE_MIN_SDK, + compileSdk: GOOGLE_COMPILE_SDK, + targetSdk: GOOGLE_COMPILE_SDK, +}); + +export const GOOGLE_PLAY_BILLING = Object.freeze({ + version: GOOGLE_PLAY_BILLING_VERSION, +}); + +export const KMP_ANDROID_SDK = Object.freeze({ + minSdk: KMP_MIN_SDK, + compileSdk: KMP_COMPILE_SDK, + targetSdk: KMP_TARGET_SDK, +}); + +export const FLUTTER_PACKAGE = Object.freeze({ + name: 'flutter_inapp_purchase', + version: FLUTTER_PACKAGE_VERSION, + installCommand: 'flutter pub add flutter_inapp_purchase', + dependencyLine: `flutter_inapp_purchase: ^${FLUTTER_PACKAGE_VERSION}`, + pubUrl: 'https://pub.dev/packages/flutter_inapp_purchase', +}); + +export const EXPO_PACKAGE = Object.freeze({ + name: 'expo-iap', + version: EXPO_PACKAGE_VERSION, + installCommand: 'npx expo install expo-iap', + dependencyLine: `"expo-iap": "^${EXPO_PACKAGE_VERSION}"`, + npmUrl: 'https://www.npmjs.com/package/expo-iap', +}); + +export const REACT_NATIVE_PACKAGE = Object.freeze({ + name: 'react-native-iap', + version: REACT_NATIVE_PACKAGE_VERSION, + installCommand: 'npm install react-native-iap', + dependencyLine: `"react-native-iap": "^${REACT_NATIVE_PACKAGE_VERSION}"`, + npmUrl: 'https://www.npmjs.com/package/react-native-iap', +}); + +export const GODOT_PACKAGE = Object.freeze({ + name: 'godot-iap', + version: GODOT_PACKAGE_VERSION, + releaseUrl: `https://github.com/hyodotdev/openiap/releases/tag/godot-iap-${GODOT_PACKAGE_VERSION}`, + downloadUrl: `https://github.com/hyodotdev/openiap/releases/download/godot-iap-${GODOT_PACKAGE_VERSION}/godot-iap-${GODOT_PACKAGE_VERSION}.zip`, +}); + +export const KMP_PACKAGE = Object.freeze({ + coordinate: 'io.github.hyochan:kmp-iap', + version: KMP_PACKAGE_VERSION, + installCommand: `implementation("io.github.hyochan:kmp-iap:${KMP_PACKAGE_VERSION}")`, + mavenUrl: `https://central.sonatype.com/artifact/io.github.hyochan/kmp-iap/${KMP_PACKAGE_VERSION}`, +}); + +export const MAUI_PACKAGE = Object.freeze({ + id: MAUI_PACKAGE_ID, + version: MAUI_PACKAGE_VERSION, + installCommand: `dotnet add package ${MAUI_PACKAGE_ID}`, + pinnedInstallCommand: `dotnet add package ${MAUI_PACKAGE_ID} --version ${MAUI_PACKAGE_VERSION}`, + packageReference: ``, + nugetUrl: `https://www.nuget.org/packages/${MAUI_PACKAGE_ID}`, + versionedNugetUrl: `https://www.nuget.org/packages/${MAUI_PACKAGE_ID}/${MAUI_PACKAGE_VERSION}`, +}); diff --git a/packages/docs/src/pages/docs/android-setup.tsx b/packages/docs/src/pages/docs/android-setup.tsx index ad90f800..e72686f9 100644 --- a/packages/docs/src/pages/docs/android-setup.tsx +++ b/packages/docs/src/pages/docs/android-setup.tsx @@ -1,4 +1,5 @@ import SEO from '../../components/SEO'; +import { OPENIAP_VERSIONS } from '../../lib/versioning'; function AndroidSetup() { return ( @@ -235,12 +236,12 @@ function AndroidSetup() {

Add the OpenIAP Android dependency:

{`// build.gradle.kts
 dependencies {
-    implementation("io.github.hyochan.openiap:openiap-google:${'$'}version")
+    implementation("io.github.hyochan.openiap:openiap-google:${OPENIAP_VERSIONS.google}")
 }
 
 // Or build.gradle (Groovy)
 dependencies {
-    implementation 'io.github.hyochan.openiap:openiap-google:${'$'}version'
+    implementation 'io.github.hyochan.openiap:openiap-google:${OPENIAP_VERSIONS.google}'
 }`}

{ console.log('User selected developer billing'); - console.log('Token:', details.externalTransactionToken); + console.log('External transaction token received; send it to your backend without logging it.'); // Process payment with your payment system const paymentResult = await processPaymentWithYourGateway({ @@ -104,7 +104,7 @@ subscription.remove();`} // Using callback openIapStore.addDeveloperProvidedBillingListener { details -> println("User selected developer billing") - println("Token: \${details.externalTransactionToken}") + println("External transaction token received; send it to your backend without logging it.") lifecycleScope.launch { // Process payment with your payment system @@ -128,7 +128,7 @@ val kmpIAP = KmpIAP() // Using callback kmpIAP.addDeveloperProvidedBillingListener { details -> println("User selected developer billing") - println("Token: \${details.externalTransactionToken}") + println("External transaction token received; send it to your backend without logging it.") lifecycleScope.launch { // Process payment with your payment system @@ -151,7 +151,7 @@ kmpIAP.addDeveloperProvidedBillingListener { details -> final subscription = FlutterInappPurchase.developerProvidedBillingStream .listen((details) async { print('User selected developer billing'); - print('Token: \${details.externalTransactionToken}'); + print('External transaction token received; send it to your backend without logging it.'); // Process payment with your payment system final paymentResult = await processPaymentWithYourGateway( diff --git a/packages/docs/src/pages/docs/events/android/user-choice-billing-listener-android.tsx b/packages/docs/src/pages/docs/events/android/user-choice-billing-listener-android.tsx index 565f9e1a..f4483832 100644 --- a/packages/docs/src/pages/docs/events/android/user-choice-billing-listener-android.tsx +++ b/packages/docs/src/pages/docs/events/android/user-choice-billing-listener-android.tsx @@ -70,7 +70,7 @@ var userChoiceBillingEvents: Flow`} const subscription = userChoiceBillingListenerAndroid(async (details) => { console.log('User chose alternative billing'); console.log('Products:', details.products); - console.log('Token:', details.externalTransactionToken); + console.log('External transaction token received; send it to your backend without logging it.'); // Process payment with your backend const paymentResult = await processPaymentWithBackend({ @@ -95,7 +95,7 @@ lifecycleScope.launch { openIapStore.userChoiceBillingEvents.collect { details -> println("User chose alternative billing") println("Products: \${details.products}") - println("Token: \${details.externalTransactionToken}") + println("External transaction token received; send it to your backend without logging it.") // Process payment with your backend val paymentResult = processPaymentWithBackend( @@ -125,7 +125,7 @@ lifecycleScope.launch { kmpIAP.userChoiceBillingEvents.collect { details -> println("User chose alternative billing") println("Products: \${details.products}") - println("Token: \${details.externalTransactionToken}") + println("External transaction token received; send it to your backend without logging it.") // Process payment with your backend val paymentResult = processPaymentWithBackend( @@ -152,7 +152,7 @@ kmpIAP.setUserChoiceBillingListener { details -> final subscription = FlutterInappPurchase.userChoiceBillingAndroid.listen((details) async { print('User chose alternative billing'); print('Products: \${details?.products}'); - print('Token: \${details?.externalTransactionToken}'); + print('External transaction token received; send it to your backend without logging it.'); // Process payment with your backend final paymentResult = await processPaymentWithBackend( diff --git a/packages/docs/src/pages/docs/example.tsx b/packages/docs/src/pages/docs/example.tsx index 1f4ab033..d7045880 100644 --- a/packages/docs/src/pages/docs/example.tsx +++ b/packages/docs/src/pages/docs/example.tsx @@ -253,7 +253,7 @@ cp OpenIapExample/Info.plist.example OpenIapExample/Info.plist # Edit Info.plist with your API key IAPKIT_API_KEY -iapkit_your_api_key_here`} +openiap-kit_`}

Local

@@ -626,7 +626,7 @@ adb install Example/build/outputs/apk/debug/Example-debug.apk`} cp local.properties.example local.properties # Add your API key -iapkit.api.key=iapkit_your_api_key_here`} +iapkit.api.key=openiap-kit_`}

Local

@@ -706,7 +706,8 @@ android { packages/google/ directory
  • - Verify it contains iapkit.api.key=your_key + Verify it contains{' '} + iapkit.api.key=openiap-kit_<your-key>
  • Clean and rebuild:{' '} @@ -886,24 +887,6 @@ android {
    {`cd libraries/react-native-iap/example
     yarn install
     yarn ios`}
    -

    - Also see the{' '} - - Expo-based example - {' '} - in the same library. -

  • { const result = await verifyPurchaseWithProvider({ provider: 'iapkit', iapkit: { - apiKey: 'your-iapkit-api-key', + apiKey: 'openiap-kit_', google: { purchaseToken: purchase.purchaseToken }, }, }); diff --git a/packages/docs/src/pages/docs/features/external-purchase.tsx b/packages/docs/src/pages/docs/features/external-purchase.tsx index c739b6d8..02c4b0bb 100644 --- a/packages/docs/src/pages/docs/features/external-purchase.tsx +++ b/packages/docs/src/pages/docs/features/external-purchase.tsx @@ -727,7 +727,7 @@ async function handleAlternativeBillingPurchase(productId: string) { // Step 4: Create reporting token (after successful payment) const token = await createAlternativeBillingTokenAndroid(); - console.log(\`Token created: \${token?.slice(0, 20)}...\`); + console.log('Token created; send it to your backend without logging it.'); // Step 5: Send token to your backend server // Backend will report token to Google Play within 24 hours @@ -792,7 +792,7 @@ suspend fun handleAlternativeBillingPurchase(productId: String) { // Step 4: Create reporting token (after successful payment) val token = iapStore.createAlternativeBillingReportingToken() - Log.d("IAP", "Token created: \${token?.take(20)}...") + Log.d("IAP", "Token created; send it to your backend without logging it.") // Step 5: Send token to your backend server // Backend will report token to Google Play within 24 hours @@ -854,7 +854,7 @@ suspend fun handleAlternativeBillingPurchase(productId: String) { // Step 4: Create reporting token (after successful payment) val token = kmpIAP.createAlternativeBillingReportingToken() - Log.d("IAP", "Token created: \${token?.take(20)}...") + Log.d("IAP", "Token created; send it to your backend without logging it.") // Step 5: Send token to your backend server // Backend will report token to Google Play within 24 hours @@ -912,7 +912,7 @@ Future handleAlternativeBillingPurchase(String productId) async { // Step 4: Create reporting token (after successful payment) final token = await FlutterInappPurchase.instance .createAlternativeBillingTokenAndroid(); - print('Token created: \${token?.substring(0, 20)}...'); + print('Token created; send it to your backend without logging it.'); // Step 5: Send token to your backend server // Backend will report token to Google Play within 24 hours @@ -972,7 +972,7 @@ Task HandleAlternativeBillingPurchaseAsync(String ProductId) { // Step 4: Create reporting token (after successful payment) var token = iapStore.createAlternativeBillingReportingToken() - Log.d("IAP", "Token created: \${token?.take(20)}...") + Log.d("IAP", "Token created; send it to your backend without logging it.") // Step 5: Send token to your backend server // Backend will report token to Google Play within 24 hours @@ -1023,7 +1023,7 @@ func handle_alternative_billing_purchase(product_id: String) -> void: # Step 4: Create reporting token (after successful payment) var token = await iap.create_alternative_billing_token_android() if token: - print("Token created: %s..." % token.substr(0, 20)) + print("Token created; send it to your backend without logging it.") # Step 5: Send token to your backend server # Backend will report token to Google Play within 24 hours @@ -1074,7 +1074,7 @@ await initConnection({ const userChoiceSubscription = userChoiceBillingListenerAndroid( async (details: UserChoiceBillingDetails) => { console.log('User selected alternative billing'); - console.log('Token:', details.externalTransactionToken); + console.log('External transaction token received; send it to your backend without logging it.'); console.log('Products:', details.products); try { @@ -1142,7 +1142,7 @@ val iapStore = OpenIapStore( // Set user choice billing listener (for alternative billing selection) iapStore.setUserChoiceBillingListener { details -> Log.d("IAP", "User selected alternative billing") - Log.d("IAP", "Token: \${details.externalTransactionToken}") + Log.d("IAP", "External transaction token received; send it to your backend without logging it.") Log.d("IAP", "Products: \${details.products}") // Process payment with your backend API @@ -1218,7 +1218,7 @@ val kmpIAP = KmpIAP( // Set user choice billing listener (for alternative billing selection) kmpIAP.setUserChoiceBillingListener { details -> Log.d("IAP", "User selected alternative billing") - Log.d("IAP", "Token: \${details.externalTransactionToken}") + Log.d("IAP", "External transaction token received; send it to your backend without logging it.") Log.d("IAP", "Products: \${details.products}") // Process payment with your backend API @@ -1293,7 +1293,7 @@ await FlutterInappPurchase.instance.initConnection( // Set user choice billing listener (for alternative billing selection) FlutterInappPurchase.userChoiceBillingStream.listen((details) async { print('User selected alternative billing'); - print('Token: \${details.externalTransactionToken}'); + print('External transaction token received; send it to your backend without logging it.'); print('Products: \${details.products}'); try { @@ -1347,7 +1347,7 @@ var iapStore = OpenIapStore( // Set user choice billing listener (for alternative billing selection) iapStore.setUserChoiceBillingListener { details -> Log.d("IAP", "User selected alternative billing") - Log.d("IAP", "Token: \${details.externalTransactionToken}") + Log.d("IAP", "External transaction token received; send it to your backend without logging it.") Log.d("IAP", "Products: \${details.products}") // Process payment with your backend API @@ -1424,7 +1424,7 @@ func _ready() -> void: func _on_user_choice_billing(details: UserChoiceBillingDetails) -> void: print("User selected alternative billing") - print("Token: %s" % details.external_transaction_token) + print("External transaction token received; send it to your backend without logging it.") print("Products: %s" % details.products) var payment_result = await your_backend.create_payment( @@ -1548,7 +1548,7 @@ async function handleExternalPurchaseWithBillingPrograms(productId: string) { // Step 4: Create reporting details (replaces createAlternativeBillingToken) const reportingDetails = await createBillingProgramReportingDetailsAndroid('EXTERNAL_OFFER'); - console.log(\`Token created: \${reportingDetails.externalTransactionToken.slice(0, 20)}...\`); + console.log('Token created; send it to your backend without logging it.'); // Step 5: Send token to your backend server await yourBackend.reportToken({ @@ -1621,7 +1621,7 @@ suspend fun handleExternalPurchaseWithBillingPrograms(productId: String) { val reportingDetails = iapStore.createBillingProgramReportingDetails( BillingProgramAndroid.ExternalOffer ) - Log.d("IAP", "Token created: \${reportingDetails.externalTransactionToken.take(20)}...") + Log.d("IAP", "Token created; send it to your backend without logging it.") // Step 5: Send token to your backend server yourBackend.reportToken( @@ -1690,7 +1690,7 @@ suspend fun handleExternalPurchaseWithBillingPrograms(productId: String) { val reportingDetails = kmpIAP.createBillingProgramReportingDetails( BillingProgramAndroid.ExternalOffer ) - Log.d("IAP", "Token created: \${reportingDetails.externalTransactionToken.take(20)}...") + Log.d("IAP", "Token created; send it to your backend without logging it.") // Step 5: Send token to your backend server yourBackend.reportToken( @@ -1757,7 +1757,7 @@ Future handleExternalPurchaseWithBillingPrograms(String productId) async { // Step 4: Create reporting details (replaces createAlternativeBillingToken) final reportingDetails = await FlutterInappPurchase.instance .createBillingProgramReportingDetailsAndroid(BillingProgramAndroid.externalOffer); - print('Token created: \${reportingDetails.externalTransactionToken.substring(0, 20)}...'); + print('Token created; send it to your backend without logging it.'); // Step 5: Send token to your backend server await yourBackend.reportToken( @@ -1824,7 +1824,7 @@ Task HandleExternalPurchaseWithBillingProgramsAsync(String ProductId) { var reportingDetails = iapStore.createBillingProgramReportingDetails( BillingProgramAndroid.ExternalOffer ) - Log.d("IAP", "Token created: \${reportingDetails.externalTransactionToken.take(20)}...") + Log.d("IAP", "Token created; send it to your backend without logging it.") // Step 5: Send token to your backend server yourBackend.reportToken( @@ -1885,7 +1885,7 @@ func handle_external_purchase_with_billing_programs(product_id: String) -> void: BillingProgramAndroid.EXTERNAL_OFFER ) if reporting_details and reporting_details.external_transaction_token: - print("Token created: %s..." % reporting_details.external_transaction_token.substr(0, 20)) + print("Token created; send it to your backend without logging it.") # Step 5: Send token to your backend server await your_backend.report_token( @@ -2011,7 +2011,7 @@ await initConnection(); const developerBillingSubscription = developerProvidedBillingListenerAndroid( async (details: DeveloperProvidedBillingDetails) => { console.log('User selected developer billing'); - console.log('External transaction token:', details.externalTransactionToken); + console.log('External transaction token received; send it to your backend without logging it.'); try { // Step 2: Process payment with your backend @@ -2092,7 +2092,7 @@ iapStore.initConnection(null) // Step 1: Set up listener for when user selects developer billing iapStore.addDeveloperProvidedBillingListener { details -> Log.d("IAP", "User selected developer billing") - Log.d("IAP", "External transaction token: \${details.externalTransactionToken}") + Log.d("IAP", "External transaction token received; send it to your backend without logging it.") // Step 2: Process payment with your backend lifecycleScope.launch { @@ -2176,7 +2176,7 @@ kmpIAP.initConnection(null) // Step 1: Set up listener for when user selects developer billing kmpIAP.addDeveloperProvidedBillingListener { details -> Log.d("IAP", "User selected developer billing") - Log.d("IAP", "External transaction token: \${details.externalTransactionToken}") + Log.d("IAP", "External transaction token received; send it to your backend without logging it.") // Step 2: Process payment with your backend lifecycleScope.launch { @@ -2259,7 +2259,7 @@ await FlutterInappPurchase.instance.initConnection(); // Step 1: Set up listener for when user selects developer billing FlutterInappPurchase.developerProvidedBillingStream.listen((details) async { print('User selected developer billing'); - print('External transaction token: \${details.externalTransactionToken}'); + print('External transaction token received; send it to your backend without logging it.'); try { // Step 2: Process payment with your backend @@ -2327,7 +2327,7 @@ iapStore.initConnection(null) // Step 1: Set up listener for when user selects developer billing iapStore.addDeveloperProvidedBillingListener { details -> Log.d("IAP", "User selected developer billing") - Log.d("IAP", "External transaction token: \${details.externalTransactionToken}") + Log.d("IAP", "External transaction token received; send it to your backend without logging it.") // Step 2: Process payment with your backend lifecycleScope.launch { @@ -2409,7 +2409,7 @@ func _ready() -> void: func _on_developer_provided_billing(details: DeveloperProvidedBillingDetails) -> void: print("User selected developer billing") - print("External transaction token: %s" % details.external_transaction_token) + print("External transaction token received; send it to your backend without logging it.") # Step 2: Process payment with your backend var payment_result = await your_backend.create_payment( 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 79abb911..31025e88 100644 --- a/packages/docs/src/pages/docs/features/offer-code-redemption.tsx +++ b/packages/docs/src/pages/docs/features/offer-code-redemption.tsx @@ -225,7 +225,7 @@ function RedeemCodeButton() { useEffect(() => { // Listen for purchases from code redemption const subscription = purchaseUpdatedListener(async (purchase) => { - console.log('Purchase from code redemption:', purchase); + console.log('Purchase from code redemption:', purchase.productId); // Verify and finish the transaction const isValid = await verifyPurchaseOnServer(purchase); diff --git a/packages/docs/src/pages/docs/features/purchase.tsx b/packages/docs/src/pages/docs/features/purchase.tsx index d73cb5a0..b3899932 100644 --- a/packages/docs/src/pages/docs/features/purchase.tsx +++ b/packages/docs/src/pages/docs/features/purchase.tsx @@ -111,7 +111,7 @@ function App() { // 2. Setup purchase success listener purchaseUpdateSubscription = purchaseUpdatedListener((purchase) => { - console.log('Purchase received:', purchase); + console.log('Purchase received:', purchase.productId); // Handle the purchase (verify + finish) void handlePurchase(purchase); }); diff --git a/packages/docs/src/pages/docs/features/subscription/index.tsx b/packages/docs/src/pages/docs/features/subscription/index.tsx index e8c23481..97a8cb7d 100644 --- a/packages/docs/src/pages/docs/features/subscription/index.tsx +++ b/packages/docs/src/pages/docs/features/subscription/index.tsx @@ -3366,9 +3366,8 @@ const checkAndroidSubscription = async () => { const purchase = subscriptionPurchases[0]; console.log('Purchase found:', purchase.productId); - console.log('Purchase token:', purchase.purchaseToken); - // Send to server for verification + // Send purchaseToken to your server for verification; do not log it. const serverResult = await fetch('https://your-server.com/api/verify-android', { method: 'POST', body: JSON.stringify({ @@ -3421,9 +3420,8 @@ suspend fun checkAndroidSubscription(): Map { val purchase = subscriptionPurchases.first() println("Purchase found: \${purchase.productId}") - println("Purchase token: \${purchase.purchaseToken}") - // Send to server for verification + // Send purchaseToken to your server for verification; do not log it. val serverResult = withContext(Dispatchers.IO) { verifyOnServer( purchaseToken = purchase.purchaseToken ?: "", @@ -3472,9 +3470,8 @@ suspend fun checkAndroidSubscription(): Map { val purchase = subscriptionPurchases.first() println("Purchase found: \${purchase.productId}") - println("Purchase token: \${purchase.purchaseToken}") - // Send to server for verification + // Send purchaseToken to your server for verification; do not log it. val serverResult = withContext(Dispatchers.IO) { verifyOnServer( purchaseToken = purchase.purchaseToken ?: "", @@ -3524,9 +3521,8 @@ Future> checkAndroidSubscription() async { final purchase = subscriptionPurchases.first; print('Purchase found: \${purchase.productId}'); - print('Purchase token: \${purchase.purchaseToken}'); - // Send to server for verification + // Send purchaseToken to your server for verification; do not log it. final response = await http.post( Uri.parse('https://your-server.com/api/verify-android'), body: jsonEncode({ @@ -3577,9 +3573,8 @@ async Task CheckAndroidSubscriptionAsync(string subscr // Purchase exists, but the client cannot determine expiry/refund/cancel state. Console.WriteLine($"Purchase found: {purchase.ProductId}"); - Console.WriteLine($"Purchase token: {purchase.PurchaseToken}"); - // Send to server for verification. + // Send purchaseToken to your server for verification; do not log it. var serverResult = await VerifyOnServerAsync( purchaseToken: purchase.PurchaseToken ?? "", productId: purchase.ProductId, @@ -3615,9 +3610,8 @@ func check_android_subscription() -> Dictionary: var purchase = subscription_purchases[0] print("Purchase found: %s" % purchase.product_id) - print("Purchase token: %s" % purchase.purchase_token) - # Send to server for verification + # Send purchaseToken to your server for verification; do not log it. var http_request = HTTPRequest.new() add_child(http_request) http_request.request( diff --git a/packages/docs/src/pages/docs/features/validation.tsx b/packages/docs/src/pages/docs/features/validation.tsx index 5594d5ab..22ea5ebb 100644 --- a/packages/docs/src/pages/docs/features/validation.tsx +++ b/packages/docs/src/pages/docs/features/validation.tsx @@ -287,7 +287,7 @@ if result.is_valid: const result = await verifyPurchaseWithProvider({ provider: 'iapkit', iapkit: { - apiKey: 'your-iapkit-api-key', + apiKey: 'openiap-kit_', apple: { jws: purchase.purchaseToken }, google: { purchaseToken: purchase.purchaseToken }, }, @@ -302,7 +302,7 @@ if (result.iapkit?.isValid && result.iapkit?.state === 'entitled') { {`let result = try await store.verifyPurchaseWithProvider( VerifyPurchaseWithProviderProps( iapkit: RequestVerifyPurchaseWithIapkitProps( - apiKey: "your-iapkit-api-key", + apiKey: "openiap-kit_", apple: RequestVerifyPurchaseWithIapkitAppleProps( jws: purchase.purchaseToken ?? "" ), @@ -316,7 +316,7 @@ if (result.iapkit?.isValid && result.iapkit?.state === 'entitled') { {`val result = module.verifyPurchaseWithProvider( VerifyPurchaseWithProviderProps( iapkit = RequestVerifyPurchaseWithIapkitProps( - apiKey = "your-api-key", + apiKey = "openiap-kit_", google = RequestVerifyPurchaseWithIapkitGoogleProps( purchaseToken = purchase.purchaseToken ) @@ -329,7 +329,7 @@ if (result.iapkit?.isValid && result.iapkit?.state === 'entitled') { {`val result = kmpIAP.verifyPurchaseWithProvider( VerifyPurchaseWithProviderProps( iapkit = RequestVerifyPurchaseWithIapkitProps( - apiKey = "your-api-key", + apiKey = "openiap-kit_", google = RequestVerifyPurchaseWithIapkitGoogleProps( purchaseToken = purchase.purchaseToken ) @@ -343,7 +343,7 @@ if (result.iapkit?.isValid && result.iapkit?.state === 'entitled') { VerifyPurchaseWithProviderProps( provider: PurchaseVerificationProvider.iapkit, iapkit: RequestVerifyPurchaseWithIapkitProps( - apiKey: 'your-iapkit-api-key', + apiKey: 'openiap-kit_', apple: RequestVerifyPurchaseWithIapkitAppleProps(jws: purchase.purchaseToken ?? ''), ), ), @@ -356,7 +356,7 @@ using OpenIap.Maui; var result = module.verifyPurchaseWithProvider( VerifyPurchaseWithProviderProps( iapkit = RequestVerifyPurchaseWithIapkitProps( - apiKey = "your-api-key", + apiKey = "openiap-kit_", google = RequestVerifyPurchaseWithIapkitGoogleProps( purchaseToken = purchase.purchaseToken ) @@ -367,7 +367,7 @@ var result = module.verifyPurchaseWithProvider( ), gdscript: ( {`var iapkit_props = RequestVerifyPurchaseWithIapkitProps.new() -iapkit_props.api_key = "your-iapkit-api-key" +iapkit_props.api_key = "openiap-kit_" iapkit_props.google = RequestVerifyPurchaseWithIapkitGoogleProps.new() iapkit_props.google.purchase_token = purchase.purchase_token diff --git a/packages/docs/src/pages/docs/horizon-setup.tsx b/packages/docs/src/pages/docs/horizon-setup.tsx index 8d264125..9b1610b8 100644 --- a/packages/docs/src/pages/docs/horizon-setup.tsx +++ b/packages/docs/src/pages/docs/horizon-setup.tsx @@ -1,5 +1,6 @@ import CodeBlock from '../../components/CodeBlock'; import SEO from '../../components/SEO'; +import { OPENIAP_VERSIONS } from '../../lib/versioning'; function HorizonSetup() { return ( @@ -50,8 +51,8 @@ function HorizonSetup() {
  • - openiap-google@1.3.2 or later installed (Horizon flavor - support added in 1.3.2) + openiap-google@{OPENIAP_VERSIONS.google} installed. + Current OpenIAP Google releases include Horizon flavor support.
  • A Meta Quest device for testing (Quest 2, Quest 3, Quest Pro)
  • diff --git a/packages/docs/src/pages/docs/kit-backend.tsx b/packages/docs/src/pages/docs/kit-backend.tsx index b55c1875..9d9c49fc 100644 --- a/packages/docs/src/pages/docs/kit-backend.tsx +++ b/packages/docs/src/pages/docs/kit-backend.tsx @@ -22,7 +22,7 @@ function KitBackend() { after a user taps "buy" — receipt validation, lifecycle webhooks, subscription state, revenue metrics, and App Store Connect / Play Console product sync — and exposes everything through one URL surface - that all five SDKs and an MCP server speak. + that the framework SDKs and MCP server speak.

    @@ -30,22 +30,22 @@ function KitBackend() { Surface map

    - Every endpoint takes the project's API key as a path segment so the - same URL works in App Store Connect, Pub/Sub push subscribers, mobile - WebViews, and stdio MCP tools without juggling bearer tokens. + Receipt verification uses an Authorization: Bearer API + key header. Webhook, subscription, product, and MCP-friendly endpoints + carry the project API key as a path segment so store consoles, mobile + WebViews, and stdio MCP tools can call them without custom bearer + header plumbing.

    • POST /v1/purchase/verify — receipt validation (Apple - JWS, Google purchaseToken, Meta Horizon). + JWS, Google purchaseToken, Meta Horizon) with a Bearer API key.
    • - POST /v1/webhooks/apple/{apiKey} — App Store - Server Notifications v2 receiver. -
    • -
    • - POST /v1/webhooks/google/{apiKey} — Google - Pub/Sub RTDN receiver (OIDC verified). + POST /v1/webhooks/{apiKey} — unified App + Store Server Notifications v2 / Google Pub/Sub RTDN receiver (Google + OIDC verified). Platform-specific /apple /{' '} + /google aliases remain supported for existing setups.
    • GET /v1/webhooks/stream/{apiKey} — SSE stream @@ -108,10 +108,9 @@ function KitBackend() { or Play Console (via the service-account JSON).
    • - Webhooks — copyable Apple ASN v2 / Google RTDN - endpoints, the SSE stream URL, and a curl recipe for emitting a - synthetic test notification without going through the App Store / - Play Console. + 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.
    @@ -260,7 +259,7 @@ if status.active: "command": "bunx", "args": ["@hyodotdev/openiap-mcp-server"], "env": { - "OPENIAP_API_KEY": "sk_live_...", + "OPENIAP_API_KEY": "openiap-kit_", "OPENIAP_BASE_URL": "https://kit.openiap.dev" } } diff --git a/packages/docs/src/pages/docs/lifecycle/subscription.tsx b/packages/docs/src/pages/docs/lifecycle/subscription.tsx index 010af7fb..9d7760ae 100644 --- a/packages/docs/src/pages/docs/lifecycle/subscription.tsx +++ b/packages/docs/src/pages/docs/lifecycle/subscription.tsx @@ -274,7 +274,7 @@ function Subscription() {

    Learn more about IAPKit integration in our{' '} diff --git a/packages/docs/src/pages/docs/setup/expo.tsx b/packages/docs/src/pages/docs/setup/expo.tsx index f5e3d5f8..c28f647e 100644 --- a/packages/docs/src/pages/docs/setup/expo.tsx +++ b/packages/docs/src/pages/docs/setup/expo.tsx @@ -1,6 +1,11 @@ import { Link } from 'react-router-dom'; import CodeBlock from '../../../components/CodeBlock'; import SEO from '../../../components/SEO'; +import { + ANDROID_SDK, + EXPO_PACKAGE, + GOOGLE_PLAY_BILLING, +} from '../../../lib/versioning'; function ExpoSetup() { return ( @@ -128,7 +133,8 @@ function ExpoSetup() {

    - expo-iap uses Google Play Billing Library v8.2, which requires{' '} + expo-iap uses Google Play Billing Library v + {GOOGLE_PLAY_BILLING.version}, which requires{' '} Kotlin 2.0+.

      @@ -240,7 +246,8 @@ export default {
      • - Requires compileSdkVersion 34+ + Requires minSdkVersion {ANDROID_SDK.minSdk}+ and{' '} + compileSdkVersion {ANDROID_SDK.compileSdk}+
      • No additional configuration needed for Expo managed workflow
      @@ -276,7 +283,7 @@ cd ios && pod install`} [ "expo-iap", { - "iapkitApiKey": "your_api_key", + "iapkitApiKey": "openiap-kit_", "modules": { "onside": true, "horizon": true @@ -570,7 +577,7 @@ switch (error.code) { "dependencies": { "react-native": "npm:react-native-tvos@0.81.5-1", "@react-native-tvos/config-tv": "^0.1.4", - "expo-iap": "latest" + ${EXPO_PACKAGE.dependencyLine} } }`} diff --git a/packages/docs/src/pages/docs/setup/flutter.tsx b/packages/docs/src/pages/docs/setup/flutter.tsx index 1c856e22..6f4da438 100644 --- a/packages/docs/src/pages/docs/setup/flutter.tsx +++ b/packages/docs/src/pages/docs/setup/flutter.tsx @@ -1,5 +1,6 @@ import CodeBlock from '../../../components/CodeBlock'; import SEO from '../../../components/SEO'; +import { ANDROID_SDK, FLUTTER_PACKAGE } from '../../../lib/versioning'; function FlutterSetup() { return ( @@ -46,7 +47,7 @@ function FlutterSetup() {

      {`dependencies: - flutter_inapp_purchase: ^9.0.0`} + ${FLUTTER_PACKAGE.dependencyLine}`}

      @@ -87,11 +88,11 @@ function FlutterSetup() {

      {`android { - compileSdkVersion 34 + compileSdkVersion ${ANDROID_SDK.compileSdk} defaultConfig { - minSdkVersion 21 // Required minimum - targetSdkVersion 34 + minSdkVersion ${ANDROID_SDK.minSdk} // Required minimum + targetSdkVersion ${ANDROID_SDK.targetSdk} // Required for v7.1.14+: Select Google Play platform missingDimensionStrategy 'platform', 'play' @@ -103,11 +104,11 @@ function FlutterSetup() {

      {`android { - compileSdk = 34 + compileSdk = ${ANDROID_SDK.compileSdk} defaultConfig { - minSdk = 21 // Required minimum - targetSdk = 34 + minSdk = ${ANDROID_SDK.minSdk} // Required minimum + targetSdk = ${ANDROID_SDK.targetSdk} // Required for v7.1.14+: Select Google Play platform missingDimensionStrategy("platform", "play") @@ -136,10 +137,8 @@ function FlutterSetup() {

      {`# In-App Purchase --keep class com.amazon.** {*;} -keep class dev.hyo.** { *; } -keep class com.android.vending.billing.** --dontwarn com.amazon.** -keepattributes *Annotation*`} diff --git a/packages/docs/src/pages/docs/setup/godot.tsx b/packages/docs/src/pages/docs/setup/godot.tsx index 2469faee..09b15123 100644 --- a/packages/docs/src/pages/docs/setup/godot.tsx +++ b/packages/docs/src/pages/docs/setup/godot.tsx @@ -376,7 +376,7 @@ func _on_connected(): print("Store connected!") func _on_purchase_updated(purchase: Dictionary): - print("Purchase: ", purchase) + print("Purchase: %s" % purchase.get("product_id", "")) func _on_purchase_error(error: Dictionary): print("Error: ", error)`} diff --git a/packages/docs/src/pages/docs/setup/kmp.tsx b/packages/docs/src/pages/docs/setup/kmp.tsx index d2075047..8a715a13 100644 --- a/packages/docs/src/pages/docs/setup/kmp.tsx +++ b/packages/docs/src/pages/docs/setup/kmp.tsx @@ -1,5 +1,13 @@ import CodeBlock from '../../../components/CodeBlock'; import SEO from '../../../components/SEO'; +import { LIBRARIES } from '../../../lib/images'; +import { KMP_ANDROID_SDK } from '../../../lib/versioning'; + +const KMP_INSTALL_COMMAND = + LIBRARIES.find(({ name }) => name === 'kmp-iap')?.installCommand ?? + 'implementation("io.github.hyochan:kmp-iap")'; +const KMP_VERSION = + KMP_INSTALL_COMMAND.match(/kmp-iap:([^")]+)/)?.[1] ?? 'x.y.z'; function KmpSetup() { return ( @@ -41,7 +49,8 @@ function KmpSetup() {

      • - Kotlin 2.1.10+ and Gradle 8.0+ + Kotlin 2.3.21+, Gradle 8.13+, and{' '} + JDK 17+
      • Active Apple Developer account (for iOS)
      • Active Google Play Developer account (for Android)
      • @@ -64,8 +73,7 @@ function KmpSetup() { {`val commonMain by getting { dependencies { - // Check Maven Central for the latest version - implementation("io.github.hyochan:kmp-iap:") + ${KMP_INSTALL_COMMAND} } }`} @@ -73,7 +81,7 @@ function KmpSetup() { {`# gradle/libs.versions.toml [versions] -kmp-iap = "" +kmp-iap = "${KMP_VERSION}" [libraries] kmp-iap = { module = "io.github.hyochan:kmp-iap", version.ref = "kmp-iap" }`} @@ -202,11 +210,11 @@ kotlin {

        {`android { - compileSdk = 34 + compileSdk = ${KMP_ANDROID_SDK.compileSdk} defaultConfig { - minSdk = 24 // Required minimum - targetSdk = 34 + minSdk = ${KMP_ANDROID_SDK.minSdk} // Required minimum + targetSdk = ${KMP_ANDROID_SDK.targetSdk} } }`} @@ -354,7 +362,7 @@ kmpIAP.endConnection()`}
      • diff --git a/packages/docs/src/pages/docs/setup/maui.tsx b/packages/docs/src/pages/docs/setup/maui.tsx index 5920fe9f..14e99987 100644 --- a/packages/docs/src/pages/docs/setup/maui.tsx +++ b/packages/docs/src/pages/docs/setup/maui.tsx @@ -1,5 +1,6 @@ import CodeBlock from '../../../components/CodeBlock'; import SEO from '../../../components/SEO'; +import { MAUI_PACKAGE } from '../../../lib/versioning'; function MauiSetup() { return ( @@ -104,15 +105,15 @@ function MauiSetup() {

        Add the package to your MAUI app project:

        - - {`dotnet add package OpenIap.Maui --version 1.0.1`} - + {MAUI_PACKAGE.installCommand}

        - Or add it directly to your app's .csproj: + This resolves the latest stable package from NuGet. If your project + pins package versions manually, use the current{' '} + NuGet package reference:

        {` - + ${MAUI_PACKAGE.packageReference} `} @@ -317,7 +318,7 @@ using OpenIap.Maui; var kit = Iap.KitApi(new KitApiOptions { - ApiKey = "", + ApiKey = "openiap-kit_", BaseUrl = "https://kit.openiap.dev", }); @@ -327,7 +328,7 @@ BindUserResponse bind = await kit.BindUserAsync(purchase.PurchaseToken!, "user_1 using WebhookListener listener = Iap.ConnectWebhookStream(new WebhookListenerOptions { - ApiKey = "", + ApiKey = "openiap-kit_", OnEvent = webhookEvent => Console.WriteLine(webhookEvent.Type), OnError = error => Console.WriteLine($"{error.Code}: {error.Message}"), }); diff --git a/packages/docs/src/pages/docs/setup/react-native.tsx b/packages/docs/src/pages/docs/setup/react-native.tsx index c9c0505a..33773fdc 100644 --- a/packages/docs/src/pages/docs/setup/react-native.tsx +++ b/packages/docs/src/pages/docs/setup/react-native.tsx @@ -1,6 +1,7 @@ import { Link } from 'react-router-dom'; import CodeBlock from '../../../components/CodeBlock'; import SEO from '../../../components/SEO'; +import { ANDROID_SDK, GOOGLE_PLAY_BILLING } from '../../../lib/versioning'; function ReactNativeSetup() { return ( @@ -15,7 +16,7 @@ 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 8.0+ on Android. + Google Play Billing {GOOGLE_PLAY_BILLING.version}+ on Android.

        • - Requires minSdkVersion 24+ and{' '} - compileSdkVersion 34+ + Requires minSdkVersion {ANDROID_SDK.minSdk}+ and{' '} + compileSdkVersion {ANDROID_SDK.compileSdk}+
        • No additional native configuration needed
        • - Uses Google Play Billing 8.0+ with automatic service reconnection + Uses Google Play Billing {GOOGLE_PLAY_BILLING.version}+ with + automatic service reconnection
        @@ -472,7 +474,8 @@ switch (error.code) { iOS: Run cd ios && bundle exec pod install
      • - Android: Ensure compileSdkVersion 34+ in{' '} + Android: Ensure{' '} + compileSdkVersion {ANDROID_SDK.compileSdk}+ in{' '} android/build.gradle
      • diff --git a/packages/docs/src/pages/docs/types/alternative-billing-types.tsx b/packages/docs/src/pages/docs/types/alternative-billing-types.tsx index cea4740d..b6e80656 100644 --- a/packages/docs/src/pages/docs/types/alternative-billing-types.tsx +++ b/packages/docs/src/pages/docs/types/alternative-billing-types.tsx @@ -325,7 +325,7 @@ await iap.init_connection()`} const userChoiceSubscription = userChoiceBillingListenerAndroid(async (details) => { console.log('User chose alternative billing'); console.log('Products:', details.products.map(p => p.productId)); - console.log('External Transaction Token:', details.externalTransactionToken); + console.log('External transaction token received; send it to your backend without logging it.'); // Process payment with your backend using the token const paymentResult = await yourBackend.processPayment({ @@ -376,7 +376,7 @@ iapStore.addUserChoiceBillingListener(object : OpenIapUserChoiceBillingListener override fun onUserChoiceBilling(details: UserChoiceBillingDetails) { Log.d("IAP", "User chose alternative billing") Log.d("IAP", "Products: \${details.products.map { it.productId }}") - Log.d("IAP", "Token: \${details.externalTransactionToken}") + Log.d("IAP", "External transaction token received; send it to your backend without logging it.") // Process payment with your backend using the token lifecycleScope.launch { @@ -431,7 +431,7 @@ final userChoiceSubscription = FlutterInappPurchase.userChoiceBillingStream .listen((details) async { print('User chose alternative billing'); print('Products: \${details.products.map((p) => p.productId).toList()}'); - print('Token: \${details.externalTransactionToken}'); + print('External transaction token received; send it to your backend without logging it.'); // Process payment with your backend using the token final paymentResult = await yourBackend.processPayment( @@ -523,7 +523,7 @@ func _on_user_choice_billing(details: UserChoiceBillingDetails): for p in details.products: product_ids.append(p.product_id) print("Products: %s" % str(product_ids)) - print("Token: %s" % details.external_transaction_token) + print("External transaction token received; send it to your backend without logging it.") # Process payment with your backend using the token var payment_result = await your_backend.process_payment( diff --git a/packages/docs/src/pages/docs/types/billing-programs.tsx b/packages/docs/src/pages/docs/types/billing-programs.tsx index f2c58f0f..17e3e387 100644 --- a/packages/docs/src/pages/docs/types/billing-programs.tsx +++ b/packages/docs/src/pages/docs/types/billing-programs.tsx @@ -479,7 +479,7 @@ await initConnection({ // Listen for developer billing selection developerProvidedBillingListenerAndroid((details) => { - console.log('Token:', details.externalTransactionToken); + console.log('External transaction token received; send it to your backend without logging it.'); // Report token to Google via your backend within 24 hours }); @@ -517,7 +517,7 @@ iapStore.initConnection( // Listen for developer billing selection iapStore.addDeveloperProvidedBillingListener { details -> - Log.d("IAP", "Token: \${details.externalTransactionToken}") + Log.d("IAP", "External transaction token received; send it to your backend without logging it.") // Report token to Google via your backend within 24 hours } @@ -557,7 +557,7 @@ await FlutterInappPurchase.instance.initConnection( // Listen for developer billing selection FlutterInappPurchase.developerProvidedBillingStream.listen((details) { - print('Token: \${details.externalTransactionToken}'); + print('External transaction token received; send it to your backend without logging it.'); // Report token to Google via your backend within 24 hours }); @@ -628,7 +628,7 @@ await iap.init_connection(config) # Listen for developer billing selection func _on_developer_provided_billing(details: DeveloperProvidedBillingDetailsAndroid): - print("Token: %s" % details.external_transaction_token) + print("External transaction token received; send it to your backend without logging it.") # Report token to Google via your backend within 24 hours iap.developer_provided_billing.connect(_on_developer_provided_billing) 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 4baf3407..1312671c 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 @@ -45,7 +45,7 @@ function VerifyPurchaseWithProviderProps() { {`).`} See{' '} diff --git a/packages/docs/src/pages/docs/types/verify-purchase-with-provider-result.tsx b/packages/docs/src/pages/docs/types/verify-purchase-with-provider-result.tsx index 47976cc6..36e3bd82 100644 --- a/packages/docs/src/pages/docs/types/verify-purchase-with-provider-result.tsx +++ b/packages/docs/src/pages/docs/types/verify-purchase-with-provider-result.tsx @@ -28,7 +28,7 @@ function VerifyPurchaseWithProviderResult() { Result envelope from verifyPurchaseWithProvider. Carries{' '} isValid plus the underlying provider response. See{' '} @@ -305,7 +305,7 @@ import type { const iosResult = await verifyPurchaseWithProvider({ provider: 'iapkit', iapkit: { - apiKey: 'your-iapkit-api-key', + apiKey: 'openiap-kit_', apple: { jws: purchase.purchaseToken, // JWS from StoreKit 2 }, @@ -316,7 +316,7 @@ const iosResult = await verifyPurchaseWithProvider({ const androidResult = await verifyPurchaseWithProvider({ provider: 'iapkit', iapkit: { - apiKey: 'your-iapkit-api-key', + apiKey: 'openiap-kit_', google: { purchaseToken: purchase.purchaseToken, }, @@ -335,7 +335,7 @@ if (result.iapkit?.isValid && result.iapkit.state === 'entitled') { // Create verification props for iOS let props = VerifyPurchaseWithProviderProps( iapkit: RequestVerifyPurchaseWithIapkitProps( - apiKey: "your-iapkit-api-key", + apiKey: "openiap-kit_", apple: RequestVerifyPurchaseWithIapkitAppleProps( jws: purchase.jwsRepresentationIOS ?? "" ), @@ -359,7 +359,7 @@ if let iapkit = result, iapkit.isValid && iapkit.state == .entitled { // Create verification props for Android val props = VerifyPurchaseWithProviderProps( iapkit = RequestVerifyPurchaseWithIapkitProps( - apiKey = "your-iapkit-api-key", + apiKey = "openiap-kit_", apple = null, google = RequestVerifyPurchaseWithIapkitGoogleProps( purchaseToken = purchase.purchaseToken @@ -386,7 +386,7 @@ result.iapkit?.let { iapkit -> final props = VerifyPurchaseWithProviderProps( provider: PurchaseVerificationProvider.iapkit, iapkit: RequestVerifyPurchaseWithIapkitProps( - apiKey: 'your-iapkit-api-key', + apiKey: 'openiap-kit_', apple: RequestVerifyPurchaseWithIapkitAppleProps( jws: purchase.jwsRepresentationIOS ?? '', ), @@ -413,7 +413,7 @@ var props = new VerifyPurchaseWithProviderProps Provider = PurchaseVerificationProvider.Iapkit, Iapkit = new RequestVerifyPurchaseWithIapkitProps { - ApiKey = "your-iapkit-api-key", + ApiKey = "openiap-kit_", Google = new RequestVerifyPurchaseWithIapkitGoogleProps { PurchaseToken = purchase.PurchaseToken ?? "", @@ -437,7 +437,7 @@ if (iapkit is { IsValid: true, State: IapkitPurchaseState.Entitled }) var props = VerifyPurchaseWithProviderProps.new() props.provider = PurchaseVerificationProvider.IAPKIT props.iapkit = RequestVerifyPurchaseWithIapkitProps.new() -props.iapkit.api_key = "your-iapkit-api-key" +props.iapkit.api_key = "openiap-kit_" props.iapkit.apple = RequestVerifyPurchaseWithIapkitAppleProps.new() props.iapkit.apple.jws = purchase.jws_representation_ios diff --git a/packages/docs/src/pages/docs/updates/releases.tsx b/packages/docs/src/pages/docs/updates/releases.tsx index dcf87a99..77cb9395 100644 --- a/packages/docs/src/pages/docs/updates/releases.tsx +++ b/packages/docs/src/pages/docs/updates/releases.tsx @@ -26,6 +26,161 @@ function Releases() { useScrollToHash(); const allNotes: Note[] = [ + // May 17, 2026 — SDK minor maintenance release + { + id: 'sdk-minor-maintenance-2026-05-17', + date: new Date('2026-05-17'), + element: ( +
        + + May 17, 2026 — SDK minor maintenance release + + +

        + Publishes native and framework SDK minor releases for repository + hardening, release automation fixes, and example cleanup. The{' '} + OpenIAP Spec remains 2.0.2; this rollout does not + add GraphQL fields, enum cases, or API operations. The minor version + signal covers SDK packaging and support-surface cleanup, including + removal of unsupported standalone Amazon and React Native Expo + example paths. +

        + +
          +
        • + Release automation — hardens the Apple, Google, + docs, and framework release workflows so version commits, tags, + artifacts, and generated release notes stay consistent across + concurrent package releases. +
        • +
        • + SDK parity and correctness — aligns framework + bridges with the native Apple and Google packages while tightening + platform-specific validation, subscription, webhook, and + alternative-billing paths. +
        • +
        • + Debugging examples — restores raw purchase, + offer, and app-account tokens in example/debug surfaces so + developers can inspect store responses during local testing. +
        • +
        • + Unsupported surfaces removed — React Native no + longer ships the Expo example; Expo users should use{' '} + expo-iap. Flutter removes the standalone Amazon + plugin path until Amazon support is formalized through the Android + flavor plan. +
        • +
        • + Documentation refresh — updates setup guides, + package ownership metadata, and OpenIAP documentation links + without changing the published spec contract. +
        • +
        + +
        +
        + ), + }, + // May 13, 2026 — OpenIAP Spec 2.0.2 purchase update replay controls { id: 'openiap-spec-2-0-2-purchase-update-replay-controls', diff --git a/packages/docs/src/pages/docs/webhooks.tsx b/packages/docs/src/pages/docs/webhooks.tsx index 6ce71707..8d178a1d 100644 --- a/packages/docs/src/pages/docs/webhooks.tsx +++ b/packages/docs/src/pages/docs/webhooks.tsx @@ -141,9 +141,10 @@ function Webhooks() {

        Tip: the lifecycle webhook URL is{' '} POST-only. Opening it in a browser shows a blank / - 404 page — that's expected. Use the dashboard's Live test{' '} - curl recipe (or App Store Connect / Pub/Sub's "Send test notification" - buttons) to verify wiring. + 404 page — that's expected. Use App Store Connect / Pub/Sub's + authenticated "Send test notification" buttons to verify production + wiring; unauthenticated curl smoke tests are only for local/dev + receivers that explicitly allow them.

        @@ -235,7 +236,7 @@ const { events, lastError, isConnected } = useWebhookEvents({ dart: ( {`import 'package:flutter_inapp_purchase/webhook_client.dart'; -final listener = connectWebhookStream(apiKey: 'sk_live_...'); +final listener = connectWebhookStream(apiKey: 'openiap-kit_'); listener.events.listen((event) { if (event.type == WebhookEventType.SubscriptionRenewed) { grantEntitlement(event.purchaseToken); @@ -274,7 +275,7 @@ when (event.type) { @onready var webhook := preload("res://addons/godot-iap/webhook_client.gd").new() func _ready() -> void: - webhook.api_key = "sk_live_..." + webhook.api_key = "openiap-kit_" webhook.event_received.connect(_on_event) add_child(webhook) webhook.connect_stream() diff --git a/packages/docs/src/pages/sponsors.tsx b/packages/docs/src/pages/sponsors.tsx index 2e275ec2..65b3057d 100644 --- a/packages/docs/src/pages/sponsors.tsx +++ b/packages/docs/src/pages/sponsors.tsx @@ -8,7 +8,7 @@ function Sponsors() { title="Sponsor OpenIAP" description="Sponsor OpenIAP — unified in-app purchase infrastructure used in production across iOS, Android, and emerging platforms. Sponsorship funds maintenance, stability, and long-term platform integration." path="/sponsors" - keywords="OpenIAP sponsors, GitHub Sponsors, IAP infrastructure, in-app purchase open source, vendor sponsorship, Amazon IAP" + keywords="OpenIAP sponsors, GitHub Sponsors, IAP infrastructure, in-app purchase open source, vendor sponsorship" />

        Sponsor OpenIAP

        diff --git a/packages/docs/src/styles/home.css b/packages/docs/src/styles/home.css index f84cf342..86dabf5c 100644 --- a/packages/docs/src/styles/home.css +++ b/packages/docs/src/styles/home.css @@ -281,47 +281,6 @@ margin-bottom: 1.5rem; } -.feature-code { - background: var(--code-bg); - border-radius: 0.5rem; - padding: 1rem; - text-align: left; - border: 1px solid var(--border-color); - position: relative; - overflow-x: auto; - overflow-y: hidden; - scrollbar-width: thin; - scrollbar-color: rgba(255, 255, 255, 0.2) transparent; -} - -.feature-code::-webkit-scrollbar { - height: 6px; -} - -.feature-code::-webkit-scrollbar-track { - background: transparent; -} - -.feature-code::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.2); - border-radius: 3px; -} - -.feature-code::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.3); -} - -.feature-code pre { - font-family: 'Consolas', 'Monaco', 'Courier New', monospace; - font-size: 0.875rem; - color: #abb2bf; - margin: 0; - padding: 0; - white-space: pre; - word-wrap: normal; - overflow-x: visible; -} - /* Home Sections with Alternating Backgrounds */ .home-section { padding: 5rem 2rem; diff --git a/packages/google/CONTRIBUTING.md b/packages/google/CONTRIBUTING.md index 7773e54b..e40c02ba 100644 --- a/packages/google/CONTRIBUTING.md +++ b/packages/google/CONTRIBUTING.md @@ -8,7 +8,7 @@ Thank you for your interest in contributing! We love your input and appreciate y 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 3. Make your changes 4. Run tests (`./gradlew :openiap:test`) -5. Commit your changes (`git commit -m 'Add amazing feature'`) +5. Commit your changes (`git commit -m 'feat(google): add amazing feature'`) 6. Push to your branch (`git push origin feature/amazing-feature`) 7. Open a Pull Request @@ -16,8 +16,8 @@ Thank you for your interest in contributing! We love your input and appreciate y ```bash # Clone your fork -git clone https://github.com/YOUR_USERNAME/openiap-google.git -cd openiap-google +git clone https://github.com/YOUR_USERNAME/openiap.git +cd openiap/packages/google # Open in Android Studio (recommended) ./scripts/open-android-studio.sh diff --git a/packages/google/Example/build.gradle.kts b/packages/google/Example/build.gradle.kts index 7005bb8c..58964f7f 100644 --- a/packages/google/Example/build.gradle.kts +++ b/packages/google/Example/build.gradle.kts @@ -1,4 +1,5 @@ import java.util.Properties +import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { id("com.android.application") @@ -13,14 +14,45 @@ if (localPropertiesFile.exists()) { localPropertiesFile.inputStream().use { localProperties.load(it) } } +val openIapBuildFile = rootProject.file("openiap/build.gradle.kts") +if (!openIapBuildFile.isFile) { + error("Google Example: missing openiap/build.gradle.kts") +} +val openIapBuild = openIapBuildFile.readText() + +fun readOpenIapAndroidInt(name: String): Int { + return Regex("""$name\s*=\s*(\d+)""") + .find(openIapBuild) + ?.groupValues + ?.get(1) + ?.toInt() + ?: error("Google Example: missing $name in ${openIapBuildFile.path}") +} + +fun readOpenIapDependencyVersion(coordinate: String): String { + return Regex("""${Regex.escape(coordinate)}:([^"$]+)""") + .find(openIapBuild) + ?.groupValues + ?.get(1) + ?: error("Google Example: missing $coordinate in ${openIapBuildFile.path}") +} + +val openIapCompileSdk = readOpenIapAndroidInt("compileSdk") +val openIapMinSdk = readOpenIapAndroidInt("minSdk") +val openIapTargetSdk = openIapCompileSdk +val openIapCoreKtxVersion = readOpenIapDependencyVersion("androidx.core:core-ktx") +val openIapLifecycleRuntimeVersion = readOpenIapDependencyVersion("androidx.lifecycle:lifecycle-runtime-ktx") +val openIapLifecycleViewModelVersion = readOpenIapDependencyVersion("androidx.lifecycle:lifecycle-viewmodel-ktx") +val openIapJunitVersion = readOpenIapDependencyVersion("junit:junit") + android { namespace = "dev.hyo.martie" - compileSdk = 35 + compileSdk = openIapCompileSdk defaultConfig { applicationId = "dev.hyo.martie" - minSdk = 24 - targetSdk = 35 + minSdk = openIapMinSdk + targetSdk = openIapTargetSdk versionCode = 1 versionName = "1.0" @@ -85,10 +117,6 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" - } - buildFeatures { compose = true buildConfig = true @@ -101,13 +129,19 @@ android { } } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + dependencies { implementation(project(":openiap")) val composeUiVersion = (project.findProperty("COMPOSE_UI_VERSION") as String?) ?: "1.6.8" - implementation("androidx.core:core-ktx:1.13.1") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") + implementation("androidx.core:core-ktx:$openIapCoreKtxVersion") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:$openIapLifecycleRuntimeVersion") implementation("androidx.activity:activity-compose:1.9.0") implementation("androidx.compose.ui:ui:$composeUiVersion") @@ -115,12 +149,12 @@ dependencies { implementation("androidx.compose.material3:material3:1.2.1") implementation("androidx.compose.material:material-icons-extended:$composeUiVersion") implementation("androidx.navigation:navigation-compose:2.7.7") - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$openIapLifecycleViewModelVersion") debugImplementation("androidx.compose.ui:ui-tooling:$composeUiVersion") debugImplementation("androidx.compose.ui:ui-test-manifest:$composeUiVersion") - testImplementation("junit:junit:4.13.2") + testImplementation("junit:junit:$openIapJunitVersion") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.compose.ui:ui-test-junit4:$composeUiVersion") diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/AlternativeBillingScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/AlternativeBillingScreen.kt index 9b703cb5..489115d3 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/AlternativeBillingScreen.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/AlternativeBillingScreen.kt @@ -40,6 +40,7 @@ import dev.hyo.openiap.ExternalLinkTypeAndroid import dev.hyo.openiap.DeveloperBillingOptionParamsAndroid import dev.hyo.openiap.DeveloperBillingLaunchModeAndroid import dev.hyo.openiap.DeveloperProvidedBillingDetailsAndroid +import dev.hyo.openiap.OpenIapLog import dev.hyo.openiap.listener.OpenIapDeveloperProvidedBillingListener import dev.hyo.martie.util.findActivity import kotlinx.coroutines.delay @@ -68,8 +69,8 @@ fun AlternativeBillingScreen(navController: NavController) { // Initialize store - use default constructor for auto-detection (compatible with both Play and Horizon) val iapStore = remember { - android.util.Log.d("AlternativeBillingScreen", "Creating OpenIapStore with auto-detection") - dev.hyo.openiap.OpenIapLog.isEnabled = true + OpenIapLog.isEnabled = true + OpenIapLog.debug("Creating OpenIapStore with auto-detection", tag = "AlternativeBillingScreen") // Use default constructor which auto-detects platform (Play or Horizon) // Alternative billing mode will be set via initConnection config @@ -79,14 +80,14 @@ fun AlternativeBillingScreen(navController: NavController) { // User Choice Billing listener (remembered to properly add/remove) val userChoiceListener = remember { dev.hyo.openiap.listener.OpenIapUserChoiceBillingListener { details -> - android.util.Log.d("UserChoiceEvent", "=== User Choice Billing Event ===") - android.util.Log.d("UserChoiceEvent", "External Token: ${details.externalTransactionToken}") - android.util.Log.d("UserChoiceEvent", "Products: ${details.products}") - android.util.Log.d("UserChoiceEvent", "==============================") + OpenIapLog.debug("=== User Choice Billing Event ===", tag = "UserChoiceEvent") + OpenIapLog.debug("External Token: ${details.externalTransactionToken}", tag = "UserChoiceEvent") + OpenIapLog.debug("Products: ${details.products}", tag = "UserChoiceEvent") + OpenIapLog.debug("==============================", tag = "UserChoiceEvent") // Show result in UI iapStore.postStatusMessage( - message = "User selected alternative billing\nToken: ${details.externalTransactionToken.take(20)}...\nProducts: ${details.products.joinToString()}", + message = "User selected alternative billing\nToken: ${details.externalTransactionToken}\nProducts: ${details.products.joinToString()}", status = dev.hyo.openiap.store.PurchaseResultStatus.Info, productId = details.products.firstOrNull() ) @@ -99,14 +100,14 @@ fun AlternativeBillingScreen(navController: NavController) { // Developer Provided Billing listener (remembered to properly add/remove) val developerBillingListener = remember { dev.hyo.openiap.listener.OpenIapDeveloperProvidedBillingListener { details -> - android.util.Log.d("DeveloperBillingEvent", "=== Developer Provided Billing Event ===") - android.util.Log.d("DeveloperBillingEvent", "External Token: ${details.externalTransactionToken}") - android.util.Log.d("DeveloperBillingEvent", "========================================") + OpenIapLog.debug("=== Developer Provided Billing Event ===", tag = "DeveloperBillingEvent") + OpenIapLog.debug("External Token: ${details.externalTransactionToken}", tag = "DeveloperBillingEvent") + OpenIapLog.debug("========================================", tag = "DeveloperBillingEvent") // Show result in UI iapStore.postStatusMessage( message = "User selected developer billing (External Payments)\n\n" + - "Token: ${details.externalTransactionToken.take(30)}...\n\n" + + "Token: ${details.externalTransactionToken}\n\n" + "⚠️ Next steps:\n" + "1. Process payment with your payment gateway\n" + "2. Report token to Google within 24 hours", @@ -157,11 +158,11 @@ fun AlternativeBillingScreen(navController: NavController) { try { val purchaseAndroid = purchase as? PurchaseAndroid if (purchaseAndroid != null) { - android.util.Log.d("AlternativeBilling", "Auto-finishing transaction for testing") + OpenIapLog.debug("Auto-finishing transaction for testing", tag = "AlternativeBilling") iapStore.finishTransaction(purchaseAndroid, true) } } catch (e: Exception) { - android.util.Log.e("AlternativeBilling", "Auto-finish failed: ${e.message}") + OpenIapLog.error("Auto-finish failed: ${e.message}", tag = "AlternativeBilling") } } } @@ -169,10 +170,10 @@ fun AlternativeBillingScreen(navController: NavController) { // Initialize connection when mode changes LaunchedEffect(selectedMode, selectedBillingProgram) { try { - android.util.Log.d("AlternativeBillingScreen", "Initializing with mode: $selectedMode") + OpenIapLog.debug("Initializing with mode: $selectedMode", tag = "AlternativeBillingScreen") // IMPORTANT: End existing connection first before creating new one - android.util.Log.d("AlternativeBillingScreen", "Ending existing connection...") + OpenIapLog.debug("Ending existing connection...", tag = "AlternativeBillingScreen") iapStore.endConnection() delay(500) // Give it time to fully disconnect @@ -196,22 +197,22 @@ fun AlternativeBillingScreen(navController: NavController) { ) } - android.util.Log.d("AlternativeBillingScreen", "Reconnecting with config: $config") + OpenIapLog.debug("Reconnecting with config: $config", tag = "AlternativeBillingScreen") val connected = iapStore.initConnection(config) - android.util.Log.d("AlternativeBillingScreen", "Connection result: $connected") + OpenIapLog.debug("Connection result: $connected", tag = "AlternativeBillingScreen") if (connected) { - android.util.Log.d("AlternativeBillingScreen", "Fetching products...") + OpenIapLog.debug("Fetching products...", tag = "AlternativeBillingScreen") val request = ProductRequest( skus = IapConstants.INAPP_SKUS, type = ProductQueryType.InApp ) iapStore.fetchProducts(request) } else { - android.util.Log.e("AlternativeBillingScreen", "Failed to connect to billing service") + OpenIapLog.error("Failed to connect to billing service", tag = "AlternativeBillingScreen") } } catch (e: Exception) { - android.util.Log.e("AlternativeBillingScreen", "Connection error: ${e.message}", e) + OpenIapLog.error("Connection error: ${e.message}", e, tag = "AlternativeBillingScreen") } } @@ -758,14 +759,14 @@ fun AlternativeBillingScreen(navController: NavController) { } // Step 3: Process payment (DEMO - not implemented) - android.util.Log.d("BillingPrograms", "⚠️ Payment processing not implemented - this is a demo") + OpenIapLog.debug("⚠️ Payment processing not implemented - this is a demo", tag = "BillingPrograms") // Step 4: Create reporting details val reportingDetails = iapStore.createBillingProgramReportingDetails(selectedBillingProgram) iapStore.postStatusMessage( "✅ Billing Programs flow completed (DEMO)\n\n" + "Program: ${reportingDetails.billingProgram}\n" + - "Token: ${reportingDetails.externalTransactionToken.take(20)}...\n\n" + + "Token: ${reportingDetails.externalTransactionToken}\n\n" + "⚠️ Next steps:\n" + "1. Process payment in your system\n" + "2. Report token to Google within 24h", @@ -773,7 +774,7 @@ fun AlternativeBillingScreen(navController: NavController) { selectedProduct!!.id ) } catch (e: Exception) { - android.util.Log.e("BillingPrograms", "Error: ${e.message}", e) + OpenIapLog.error("Error: ${e.message}", e, tag = "BillingPrograms") iapStore.postStatusMessage( "Error: ${e.message}", PurchaseResultStatus.Error @@ -837,14 +838,14 @@ fun AlternativeBillingScreen(navController: NavController) { } // Step 2.5: Process payment (DEMO - not implemented) - android.util.Log.d("AlternativeBilling", "⚠️ Payment processing not implemented") + OpenIapLog.debug("⚠️ Payment processing not implemented", tag = "AlternativeBilling") // Step 3: Create token @Suppress("DEPRECATION") val token = iapStore.createAlternativeBillingReportingToken() if (token != null) { iapStore.postStatusMessage( - "Alternative billing completed (DEMO)\nToken: ${token.take(20)}...\n⚠️ Backend reporting required", + "Alternative billing completed (DEMO)\nToken: $token\n⚠️ Backend reporting required", PurchaseResultStatus.Info, selectedProduct!!.id ) @@ -855,7 +856,7 @@ fun AlternativeBillingScreen(navController: NavController) { ) } } catch (e: Exception) { - android.util.Log.e("AlternativeBilling", "Legacy alternative billing error: ${e.message}", e) + OpenIapLog.error("Legacy alternative billing error: ${e.message}", e, tag = "AlternativeBilling") iapStore.postStatusMessage( "Alternative billing failed: ${e.message}", PurchaseResultStatus.Error @@ -905,7 +906,7 @@ fun AlternativeBillingScreen(navController: NavController) { // If user selects Google Play → onPurchaseUpdated callback // If user selects alternative → UserChoiceBillingListener callback } catch (e: Exception) { - android.util.Log.e("AlternativeBilling", "User choice billing error: ${e.message}", e) + OpenIapLog.error("User choice billing error: ${e.message}", e, tag = "AlternativeBilling") iapStore.postStatusMessage( "User choice billing failed: ${e.message}", PurchaseResultStatus.Error @@ -971,7 +972,7 @@ fun AlternativeBillingScreen(navController: NavController) { type = ProductQueryType.InApp ) - android.util.Log.d("ExternalPayments", "Launching purchase with External Payments option") + OpenIapLog.debug("Launching purchase with External Payments option", tag = "ExternalPayments") iapStore.requestPurchase(props) // If user selects Google Play → onPurchaseSuccess callback @@ -984,7 +985,7 @@ fun AlternativeBillingScreen(navController: NavController) { selectedProduct!!.id ) } catch (e: Exception) { - android.util.Log.e("ExternalPayments", "External Payments error: ${e.message}", e) + OpenIapLog.error("External Payments error: ${e.message}", e, tag = "ExternalPayments") iapStore.postStatusMessage( "External Payments failed: ${e.message}", PurchaseResultStatus.Error @@ -1043,7 +1044,7 @@ fun AlternativeBillingScreen(navController: NavController) { style = MaterialTheme.typography.bodySmall ) Text( - "Token: ${purchase.purchaseToken?.take(20)}...", + "Token: ${purchase.purchaseToken}", style = MaterialTheme.typography.bodySmall ) 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 769e67f8..c904c5d6 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 @@ -563,7 +563,7 @@ fun PurchaseFlowScreen( ?: throw IllegalStateException("Purchase token is required for IAPKit verification") println("PurchaseFlow: IAPKit verification params:") - println(" - purchaseToken: ${token.take(6)}… (redacted)") + println(" - purchaseToken: $token") val props = RequestVerifyPurchaseWithIapkitProps( apiKey = apiKey, @@ -651,7 +651,6 @@ fun PurchaseFlowScreen( result } catch (e: Exception) { println("PurchaseFlow: IAPKit verification error: ${e.message}") - e.printStackTrace() verificationResultMessage = "❌ IAPKit verification error: ${e.message}" iapStore.postStatusMessage( message = "Verification error: ${e.message}. Finishing transaction anyway for testing.", diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt index 3499df6c..04290ccb 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt @@ -187,7 +187,6 @@ fun SubscriptionFlowScreen( } } catch (e: Exception) { println("SubscriptionFlow: getActiveSubscriptions FAILED: ${e.message}") - e.printStackTrace() } delay(500) @@ -220,7 +219,7 @@ fun SubscriptionFlowScreen( println(" Offer $index:") println(" Base Plan: ${offer.basePlanId}") println(" Offer ID: ${offer.offerId}") - println(" Offer Token: ${offer.offerToken.take(20)}...") + println(" Offer Token: ${offer.offerToken}") offer.pricingPhases.pricingPhaseList.forEachIndexed { phaseIndex, phase -> println(" Phase $phaseIndex: ${phase.formattedPrice} for ${phase.billingPeriod}") } @@ -235,7 +234,6 @@ fun SubscriptionFlowScreen( } } catch (e: Exception) { println("SubscriptionFlow: Initialization error: ${e.message}") - e.printStackTrace() iapStore.postStatusMessage( message = "Failed to initialize: ${e.message}", status = PurchaseResultStatus.Error @@ -859,7 +857,7 @@ fun SubscriptionFlowScreen( return@launch } - println("SubscriptionFlow [Horizon/Play]: Changing from ${currentOffer.basePlanId} to ${targetOffer.basePlanId} with token: ${purchaseToken.take(10)}...") + println("SubscriptionFlow [Horizon/Play]: Changing from ${currentOffer.basePlanId} to ${targetOffer.basePlanId} with token: $purchaseToken") // Request subscription offer change (same product, different offer) // Using new subscriptionProductReplacementParams API (8.1.0+) @@ -908,7 +906,6 @@ fun SubscriptionFlowScreen( } } catch (e: Exception) { println("SubscriptionFlow: Error changing subscription: ${e.message}") - e.printStackTrace() iapStore.postStatusMessage( message = "Subscription change failed: ${e.message}", @@ -953,7 +950,7 @@ fun SubscriptionFlowScreen( } // Log purchase details for debugging - println("SubscriptionFlow: Current purchase details - productId: ${subscription.productId}, token: ${subscription.purchaseToken?.take(10)}") + println("SubscriptionFlow: Current purchase details - productId: ${subscription.productId}, token: ${subscription.purchaseToken}") println("SubscriptionFlow: Purchase state: ${subscription.purchaseState}") // Resolve the active offer for this subscription @@ -1087,7 +1084,7 @@ fun SubscriptionFlowScreen( return@launch } - println("SubscriptionFlow: Changing from ${currentOffer.basePlanId} to ${targetOffer.basePlanId} with token: ${purchaseToken.take(10)}...") + println("SubscriptionFlow: Changing from ${currentOffer.basePlanId} to ${targetOffer.basePlanId} with token: $purchaseToken") // For same subscription with different offers, use CHARGE_FULL_PRICE // This is often the only supported mode for offer changes @@ -1137,7 +1134,6 @@ fun SubscriptionFlowScreen( } } catch (e: Exception) { println("SubscriptionFlow: Error changing subscription: ${e.message}") - e.printStackTrace() // If upgrade fails, show more helpful message val errorMessage = when { @@ -1228,7 +1224,7 @@ fun SubscriptionFlowScreen( } // Platform-specific offer selection - val subscriptionOffers = if (isHorizon && product.id == "dev.hyo.martie.premium" && product is ProductAndroid) { + val subscriptionOffers = if (isHorizon && product.id == "dev.hyo.martie.premium") { // HORIZON ONLY: Premium product has multiple offers (MONTHLY and ANNUAL) // We default to MONTHLY offer for initial purchase val monthlyOffer = product.subscriptionOfferDetailsAndroid?.find { offer -> @@ -1454,7 +1450,6 @@ fun SubscriptionFlowScreen( result } catch (e: Exception) { println("SubscriptionFlow: IAPKit verification error: ${e.message}") - e.printStackTrace() verificationResultMessage = "❌ IAPKit verification error: ${e.message}" iapStore.postStatusMessage( message = "Verification error: ${e.message}. Finishing transaction anyway for testing.", diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/WebhookStreamScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/WebhookStreamScreen.kt index f41510a2..3d40680d 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/WebhookStreamScreen.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/WebhookStreamScreen.kt @@ -110,7 +110,7 @@ fun WebhookStreamScreen(navController: NavController) { ) { Text("SSE /v1/webhooks/stream/{apiKey}", style = MaterialTheme.typography.titleMedium) Text( - "api key: ${BuildConfig.IAPKIT_API_KEY.take(8).ifEmpty { "MISSING" }}", + "api key: ${if (BuildConfig.IAPKIT_API_KEY.isBlank()) "MISSING" else "CONFIGURED"}", color = AppColors.textSecondary ) diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/uis/Modals.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/uis/Modals.kt index 0abf369c..1a6e0edb 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/uis/Modals.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/uis/Modals.kt @@ -364,7 +364,7 @@ fun PurchaseDetailModal( } add("id" to purchase.id) add("transactionId" to (purchase.transactionId ?: "-")) - add("purchaseToken" to (purchase.purchaseToken ?: "-")) + add("purchaseToken" to displayIfPresent(purchase.purchaseToken)) add("purchaseState" to purchase.purchaseState.rawValue) add("productId" to purchase.productId) add("transactionDate" to purchase.transactionDate.toString()) @@ -412,3 +412,6 @@ private fun DetailRow(label: String, value: String) { Text(value, style = MaterialTheme.typography.bodyMedium) } } + +private fun displayIfPresent(value: String?): String = + if (value.isNullOrEmpty()) "-" else value diff --git a/packages/google/README.md b/packages/google/README.md index 07870bd9..09c92b89 100644 --- a/packages/google/README.md +++ b/packages/google/README.md @@ -3,13 +3,13 @@
        OpenIAP Google Logo -

        Android implementation of the OpenIAP specification using Google Play Billing.

        +

        Android implementation of the OpenIAP specification using Google Play Billing.


        [![Maven Central](https://img.shields.io/maven-central/v/io.github.hyochan.openiap/openiap-google)](https://central.sonatype.com/artifact/io.github.hyochan.openiap/openiap-google) -[![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21) +[![API](https://img.shields.io/badge/API-23%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=23) [![Google Release](https://github.com/hyodotdev/openiap/actions/workflows/release-google.yml/badge.svg)](https://github.com/hyodotdev/openiap/actions/workflows/release-google.yml) [![CI](https://github.com/hyodotdev/openiap/actions/workflows/ci.yml/badge.svg)](https://github.com/hyodotdev/openiap/actions/workflows/ci.yml) [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) @@ -31,10 +31,10 @@ Visit [**openiap.dev**](https://openiap.dev) for complete documentation, API ref ## Requirements -- **Minimum SDK**: 21 (Android 5.0) -- **Compile SDK**: 34+ -- **Google Play Billing**: v8.0.0 -- **Kotlin**: 1.9.20+ +- **Minimum SDK**: 23 (Android 6.0) +- **Compile SDK**: 35 +- **Google Play Billing**: v8.3.0 +- **Kotlin**: 2.2.0+ ## Installation @@ -42,11 +42,11 @@ Add to your module's `build.gradle.kts`: ```kotlin dependencies { - implementation("io.github.hyochan.openiap:openiap-google:$version") + implementation("io.github.hyochan.openiap:openiap-google:") } ``` -> Check [`openiap-versions.json`](../../openiap-versions.json) for the current version. +Use the latest version from [Maven Central](https://central.sonatype.com/artifact/io.github.hyochan.openiap/openiap-google) or the badge above. ## Quick Start diff --git a/packages/google/build.gradle.kts b/packages/google/build.gradle.kts index 79992dbb..4c0fa773 100644 --- a/packages/google/build.gradle.kts +++ b/packages/google/build.gradle.kts @@ -1,36 +1,29 @@ plugins { - id("com.android.library") version "8.7.3" apply false - id("com.android.application") version "8.7.3" apply false + id("com.android.library") version "8.13.2" apply false + id("com.android.application") version "8.13.2" 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.29.0" apply false + id("com.vanniktech.maven.publish") version "0.35.0" apply false } +import groovy.json.JsonSlurper import java.io.File // Read version from monorepo root or environment variable val androidVersion = System.getenv("ORG_GRADLE_PROJECT_openIapVersion") ?: run { - // Fallback: read from openiap-versions.json val versionsFile = File(rootDir.parentFile.parentFile, "openiap-versions.json") - val jsonText = versionsFile.readText() - jsonText.substringAfter("\"google\": \"").substringBefore("\"") -} - -val gqlVersion = run { - val versionsFile = File(rootDir.parentFile.parentFile, "openiap-versions.json") - if (versionsFile.exists()) { - val jsonText = versionsFile.readText() - jsonText.substringAfter("\"gql\": \"").substringBefore("\"") - } else { - "1.2.2" // Fallback + if (!versionsFile.isFile) { + error("packages/google: missing openiap-versions.json at ${versionsFile.path}") } + val versionsJson = JsonSlurper().parseText(versionsFile.readText()) as Map<*, *> + versionsJson["google"]?.toString() + ?: error("packages/google: 'google' version missing in openiap-versions.json") } extra["OPENIAP_VERSION"] = androidVersion -extra["GQL_VERSION"] = gqlVersion -// Configure Sonatype (OSSRH) publishing at the root -// Credentials are sourced from env or gradle.properties (OSSRH_USERNAME/OSSRH_PASSWORD) +// Configure Maven Central publishing at the root. +// Credentials are sourced from env or gradle.properties. // Maven Central publishing is configured per-module via Vanniktech plugin. tasks.register("clean", Delete::class) { diff --git a/packages/google/gradle/wrapper/gradle-wrapper.properties b/packages/google/gradle/wrapper/gradle-wrapper.properties index 79eb9d00..ed4c299a 100644 --- a/packages/google/gradle/wrapper/gradle-wrapper.properties +++ b/packages/google/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/packages/google/openiap/build.gradle.kts b/packages/google/openiap/build.gradle.kts index 9ceb3b9d..ba1b36ff 100644 --- a/packages/google/openiap/build.gradle.kts +++ b/packages/google/openiap/build.gradle.kts @@ -1,4 +1,5 @@ import groovy.json.JsonSlurper +import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { id("com.android.library") @@ -9,8 +10,12 @@ plugins { // 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}") +} val versionsJson = JsonSlurper().parseText(versionsFile.readText()) as Map<*, *> -val openIapVersion: String = versionsJson["google"]?.toString() ?: "1.0.0" +val openIapVersion: String = versionsJson["google"]?.toString() + ?: error("packages/google: 'google' version missing in openiap-versions.json") android { namespace = "io.github.hyochan.openiap" @@ -53,10 +58,6 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" - } - // Enable Compose for composables in this library (IapContext) buildFeatures { compose = true @@ -89,7 +90,18 @@ android { } } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + dependencies { + val playBillingVersion = "8.3.0" + val coroutinesVersion = "1.9.0" + val horizonPlatformVersion = "77.0.1" + val horizonBillingCompatibilityVersion = "1.1.1" + implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") @@ -99,18 +111,18 @@ dependencies { // Play flavor: Google Play Billing API (compile + runtime) // Version 8.3.0 adds External Payments Program support (Japan only) - add("playCompileOnly", "com.android.billingclient:billing-ktx:8.3.0") - add("playApi", "com.android.billingclient:billing-ktx:8.3.0") + add("playCompileOnly", "com.android.billingclient:billing-ktx:$playBillingVersion") + add("playApi", "com.android.billingclient:billing-ktx:$playBillingVersion") // Horizon flavor: Meta Horizon Platform SDK and Billing Compatibility Library (compile + runtime) - add("horizonCompileOnly", "com.meta.horizon.platform.ovr:android-platform-sdk:77.0.1") - add("horizonApi", "com.meta.horizon.platform.ovr:android-platform-sdk:77.0.1") - add("horizonCompileOnly", "com.meta.horizon.billingclient.api:horizon-billing-compatibility:1.1.1") - add("horizonApi", "com.meta.horizon.billingclient.api:horizon-billing-compatibility:1.1.1") + add("horizonCompileOnly", "com.meta.horizon.platform.ovr:android-platform-sdk:$horizonPlatformVersion") + add("horizonApi", "com.meta.horizon.platform.ovr:android-platform-sdk:$horizonPlatformVersion") + add("horizonCompileOnly", "com.meta.horizon.billingclient.api:horizon-billing-compatibility:$horizonBillingCompatibilityVersion") + add("horizonApi", "com.meta.horizon.billingclient.api:horizon-billing-compatibility:$horizonBillingCompatibilityVersion") // Kotlin Coroutines - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7") // JSON handling @@ -123,9 +135,9 @@ dependencies { // Testing dependencies testImplementation("junit:junit:4.13.2") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") // Add Google Play Billing for tests (all flavors need it for OpenIapErrorTest) - testImplementation("com.android.billingclient:billing-ktx:8.3.0") + testImplementation("com.android.billingclient:billing-ktx:$playBillingVersion") // Robolectric for lightweight Android JVM tests (e.g. Horizon no-op listener) testImplementation("org.robolectric:robolectric:4.13") testImplementation("androidx.test:core:1.5.0") @@ -176,8 +188,8 @@ mavenPublishing { } } - // Use the new Central Portal publishing which avoids Nexus staging profile lookups. - publishToMavenCentral(com.vanniktech.maven.publish.SonatypeHost.CENTRAL_PORTAL) + // Central Portal is the default Maven Central target on Vanniktech 0.33+. + publishToMavenCentral() signAllPublications() pom { diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt index 56778879..8cea7ea4 100644 --- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt @@ -2,7 +2,6 @@ package dev.hyo.openiap import android.app.Activity import android.content.Context -import android.util.Log import com.meta.horizon.billingclient.api.AcknowledgePurchaseParams import com.meta.horizon.billingclient.api.AlternativeBillingOnlyInformationDialogListener import com.meta.horizon.billingclient.api.AlternativeBillingOnlyReportingDetails @@ -419,9 +418,9 @@ class OpenIapModule( if (androidArgs.type == ProductQueryType.Subs) { val availableOffers = productDetails.subscriptionOfferDetails?.map { - "${it.basePlanId}:${it.offerToken}" + it.basePlanId } ?: emptyList() - OpenIapLog.d("Available offers for ${productDetails.productId}: $availableOffers", TAG) + OpenIapLog.d("Available offer base plans for ${productDetails.productId}: $availableOffers", TAG) val availableTokens = productDetails.subscriptionOfferDetails?.map { it.offerToken } ?: emptyList() val fromQueue = requestedOffersBySku[productDetails.productId]?.let { queue -> @@ -431,10 +430,9 @@ class OpenIapModule( val resolved = fromQueue ?: fromIndex ?: productDetails.subscriptionOfferDetails?.firstOrNull()?.offerToken OpenIapLog.d("Resolved offer token for ${productDetails.productId}: $resolved", TAG) - android.util.Log.i(TAG, "BILLING_FLOW_PARAM: SKU=${productDetails.productId}, resolvedOfferToken=$resolved") if (resolved.isNullOrEmpty() || (availableTokens.isNotEmpty() && !availableTokens.contains(resolved))) { - OpenIapLog.w("Invalid offer token: $resolved not in $availableTokens", TAG) + OpenIapLog.w("Invalid offer token: $resolved not in available offer tokens", TAG) val err = OpenIapError.SkuOfferMismatch purchaseErrorListeners.forEach { listener -> runCatching { listener.onPurchaseError(err) } } consumePurchaseCallback(Result.success(emptyList())) @@ -474,7 +472,7 @@ class OpenIapModule( if (androidArgs.type == ProductQueryType.Subs && !androidArgs.purchaseToken.isNullOrBlank()) { // This is a subscription upgrade/downgrade - do not set obfuscatedProfileId OpenIapLog.d("=== Subscription Upgrade Flow ===", TAG) - OpenIapLog.d(" - Old Token: ${androidArgs.purchaseToken.take(10)}...", TAG) + OpenIapLog.d(" - Old Token: ${androidArgs.purchaseToken}", TAG) OpenIapLog.d(" - Target SKUs: ${androidArgs.skus}", TAG) OpenIapLog.d(" - Replacement mode: ${androidArgs.replacementMode}", TAG) OpenIapLog.d(" - Product Details Count: ${paramsList.size}", TAG) @@ -843,10 +841,8 @@ class OpenIapModule( OpenIapLog.i("Purchases count: ${purchases?.size ?: 0}", TAG) purchases?.forEachIndexed { index, purchase -> - val redactedToken = purchase.purchaseToken?.take(8)?.plus("…") - val redactedOrder = purchase.orderId?.take(8)?.plus("…") OpenIapLog.i( - "[HorizonPurchase $index] productIds=${purchase.products} token=$redactedToken orderId=$redactedOrder " + + "[HorizonPurchase $index] productIds=${purchase.products} token=${purchase.purchaseToken} orderId=${purchase.orderId} " + "acknowledged=${purchase.isAcknowledged()} autoRenew=${purchase.isAutoRenewing()}", TAG ) @@ -1005,7 +1001,7 @@ class OpenIapModule( OpenIapLog.w("Alternative Billing not supported by Horizon library", TAG) cont.resumeWithException(Exception("Feature not supported")) } catch (e: Exception) { - Log.e(TAG, "Error checking alternative billing: ${e.message}") + OpenIapLog.e("Error checking alternative billing: ${e.message}", e, TAG) cont.resumeWithException(e) } } @@ -1015,7 +1011,7 @@ class OpenIapModule( } catch (e: OpenIapError) { throw e } catch (e: Exception) { - Log.e(TAG, "Error in checkAlternativeBillingAvailability: ${e.message}") + OpenIapLog.e("Error in checkAlternativeBillingAvailability: ${e.message}", e, TAG) false } } @@ -1043,7 +1039,7 @@ class OpenIapModule( OpenIapLog.w("showAlternativeBillingOnlyInformationDialog not supported", TAG) cont.resumeWithException(Exception("Feature not supported")) } catch (e: Exception) { - Log.e(TAG, "Error showing alternative billing dialog: ${e.message}") + OpenIapLog.e("Error showing alternative billing dialog: ${e.message}", e, TAG) cont.resumeWithException(e) } } @@ -1053,7 +1049,7 @@ class OpenIapModule( } catch (e: OpenIapError) { throw e } catch (e: Exception) { - Log.e(TAG, "Error in showAlternativeBillingInformationDialog: ${e.message}") + OpenIapLog.e("Error in showAlternativeBillingInformationDialog: ${e.message}", e, TAG) false } } @@ -1072,7 +1068,7 @@ class OpenIapModule( OpenIapLog.w("createAlternativeBillingOnlyReportingDetails not supported", TAG) cont.resumeWithException(Exception("Feature not supported")) } catch (e: Exception) { - Log.e(TAG, "Error creating alternative billing token: ${e.message}") + OpenIapLog.e("Error creating alternative billing token: ${e.message}", e, TAG) cont.resumeWithException(e) } } @@ -1086,57 +1082,57 @@ class OpenIapModule( } catch (e: OpenIapError) { throw e } catch (e: Exception) { - Log.e(TAG, "Error in createAlternativeBillingReportingToken: ${e.message}") + OpenIapLog.e("Error in createAlternativeBillingReportingToken: ${e.message}", e, TAG) null } } override fun setUserChoiceBillingListener(listener: dev.hyo.openiap.listener.UserChoiceBillingListener?) { // No-op: User Choice Billing is a Google Play feature, not supported on Meta Horizon - Log.w(TAG, "setUserChoiceBillingListener is not supported on Meta Horizon (no-op)") + OpenIapLog.w("setUserChoiceBillingListener is not supported on Meta Horizon (no-op)", TAG) } override fun setDeveloperProvidedBillingListener(listener: dev.hyo.openiap.listener.DeveloperProvidedBillingListener?) { // No-op: External Payments is a Google Play 8.3.0+ feature, not supported on Meta Horizon - Log.w(TAG, "setDeveloperProvidedBillingListener is not supported on Meta Horizon (no-op)") + OpenIapLog.w("setDeveloperProvidedBillingListener is not supported on Meta Horizon (no-op)", TAG) } override fun addUserChoiceBillingListener(listener: OpenIapUserChoiceBillingListener) { // No-op: User Choice Billing is a Google Play feature, not supported on Meta Horizon - Log.w(TAG, "addUserChoiceBillingListener is not supported on Meta Horizon (no-op)") + OpenIapLog.w("addUserChoiceBillingListener is not supported on Meta Horizon (no-op)", TAG) } override fun removeUserChoiceBillingListener(listener: OpenIapUserChoiceBillingListener) { // No-op: User Choice Billing is a Google Play feature, not supported on Meta Horizon - Log.w(TAG, "removeUserChoiceBillingListener is not supported on Meta Horizon (no-op)") + OpenIapLog.w("removeUserChoiceBillingListener is not supported on Meta Horizon (no-op)", TAG) } override fun addDeveloperProvidedBillingListener(listener: OpenIapDeveloperProvidedBillingListener) { // No-op: External Payments is a Google Play 8.3.0+ feature, not supported on Meta Horizon - Log.w(TAG, "addDeveloperProvidedBillingListener is not supported on Meta Horizon (no-op)") + OpenIapLog.w("addDeveloperProvidedBillingListener is not supported on Meta Horizon (no-op)", TAG) } override fun removeDeveloperProvidedBillingListener(listener: OpenIapDeveloperProvidedBillingListener) { // No-op: External Payments is a Google Play 8.3.0+ feature, not supported on Meta Horizon - Log.w(TAG, "removeDeveloperProvidedBillingListener is not supported on Meta Horizon (no-op)") + OpenIapLog.w("removeDeveloperProvidedBillingListener is not supported on Meta Horizon (no-op)", TAG) } override fun addSubscriptionBillingIssueListener(listener: dev.hyo.openiap.listener.OpenIapSubscriptionBillingIssueListener) { // No-op: Suspended-subscription detection (Purchase.isSuspended) requires Google Play // Billing Library 8.1+. The Meta Horizon Billing Compatibility SDK targets Play Billing 7.0 // and does not expose this signal. - Log.w(TAG, "addSubscriptionBillingIssueListener is not supported on Meta Horizon (no-op); requires Play Billing 8.1+") + OpenIapLog.w("addSubscriptionBillingIssueListener is not supported on Meta Horizon (no-op); requires Play Billing 8.1+", TAG) } override fun removeSubscriptionBillingIssueListener(listener: dev.hyo.openiap.listener.OpenIapSubscriptionBillingIssueListener) { // No-op: see addSubscriptionBillingIssueListener - Log.w(TAG, "removeSubscriptionBillingIssueListener is not supported on Meta Horizon (no-op)") + OpenIapLog.w("removeSubscriptionBillingIssueListener is not supported on Meta Horizon (no-op)", TAG) } // Billing Programs (8.2.0+, EXTERNAL_PAYMENTS 8.3.0+) - Not supported on Horizon override suspend fun isBillingProgramAvailable(program: BillingProgramAndroid): BillingProgramAvailabilityResultAndroid { // No-op: Billing Programs is a Google Play 8.2.0+ feature, not supported on Meta Horizon - Log.w(TAG, "isBillingProgramAvailable is not supported on Meta Horizon (no-op)") + OpenIapLog.w("isBillingProgramAvailable is not supported on Meta Horizon (no-op)", TAG) return BillingProgramAvailabilityResultAndroid( billingProgram = program, isAvailable = false @@ -1145,7 +1141,7 @@ class OpenIapModule( override suspend fun createBillingProgramReportingDetails(program: BillingProgramAndroid): BillingProgramReportingDetailsAndroid { // No-op: Billing Programs is a Google Play 8.2.0+ feature, not supported on Meta Horizon - Log.w(TAG, "createBillingProgramReportingDetails is not supported on Meta Horizon (no-op)") + OpenIapLog.w("createBillingProgramReportingDetails is not supported on Meta Horizon (no-op)", TAG) return BillingProgramReportingDetailsAndroid( billingProgram = program, externalTransactionToken = "" @@ -1154,7 +1150,7 @@ class OpenIapModule( override suspend fun launchExternalLink(activity: Activity, params: LaunchExternalLinkParamsAndroid): Boolean { // No-op: Billing Programs is a Google Play 8.2.0+ feature, not supported on Meta Horizon - Log.w(TAG, "launchExternalLink is not supported on Meta Horizon (no-op)") + OpenIapLog.w("launchExternalLink is not supported on Meta Horizon (no-op)", TAG) return false } } diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/Helpers.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/Helpers.kt index 2df94910..dd09dfeb 100644 --- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/Helpers.kt +++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/Helpers.kt @@ -17,26 +17,21 @@ private const val TAG = "Helpers" * Query and restore all purchases (both INAPP and SUBS) for Horizon */ internal suspend fun restorePurchasesHorizon(client: BillingClient?): List { - android.util.Log.i("HORIZON_QUERY", "restorePurchasesHorizon: Starting") OpenIapLog.d("restorePurchasesHorizon: Starting", TAG) if (client == null) { - android.util.Log.w("HORIZON_QUERY", "restorePurchasesHorizon: BillingClient is null") OpenIapLog.w("restorePurchasesHorizon: BillingClient is null", TAG) return emptyList() } val purchases = mutableListOf() val inapp = queryPurchasesHorizon(client, BillingClient.ProductType.INAPP) - android.util.Log.i("HORIZON_QUERY", "restorePurchasesHorizon: INAPP purchases = ${inapp.size}") OpenIapLog.d("restorePurchasesHorizon: INAPP purchases = ${inapp.size}", TAG) purchases += inapp val subs = queryPurchasesHorizon(client, BillingClient.ProductType.SUBS) - android.util.Log.i("HORIZON_QUERY", "restorePurchasesHorizon: SUBS purchases = ${subs.size}") OpenIapLog.d("restorePurchasesHorizon: SUBS purchases = ${subs.size}", TAG) purchases += subs - android.util.Log.i("HORIZON_QUERY", "restorePurchasesHorizon: Total = ${purchases.size}") OpenIapLog.d("restorePurchasesHorizon: Total = ${purchases.size}", TAG) return purchases } @@ -48,11 +43,9 @@ internal suspend fun queryPurchasesHorizon( client: BillingClient?, productType: String ): List = suspendCancellableCoroutine { continuation -> - android.util.Log.i("HORIZON_QUERY", "queryPurchasesHorizon: type=$productType") OpenIapLog.d("queryPurchasesHorizon: type=$productType", TAG) val billingClient = client ?: run { - android.util.Log.w("HORIZON_QUERY", "queryPurchasesHorizon: BillingClient is null") OpenIapLog.w("queryPurchasesHorizon: BillingClient is null", TAG) continuation.resume(emptyList()) return@suspendCancellableCoroutine @@ -60,16 +53,14 @@ internal suspend fun queryPurchasesHorizon( // CRITICAL FIX: Check if BillingClient is ready before querying if (!billingClient.isReady()) { - android.util.Log.w("HORIZON_QUERY", "queryPurchasesHorizon: BillingClient is not ready, returning empty list") OpenIapLog.w("queryPurchasesHorizon: BillingClient is not ready", TAG) continuation.resume(emptyList()) return@suspendCancellableCoroutine } - android.util.Log.i("HORIZON_QUERY", "queryPurchasesHorizon: BillingClient is ready, querying purchases") + OpenIapLog.d("queryPurchasesHorizon: BillingClient is ready, querying purchases", TAG) val params = QueryPurchasesParams.newBuilder().setProductType(productType).build() billingClient.queryPurchasesAsync(params) { result, purchaseList -> - android.util.Log.i("HORIZON_QUERY", "queryPurchasesHorizon: type=$productType responseCode=${result.responseCode} count=${purchaseList?.size ?: 0}") OpenIapLog.d( "queryPurchasesHorizon: type=$productType responseCode=${result.responseCode} " + "count=${purchaseList?.size ?: 0}", @@ -78,14 +69,12 @@ internal suspend fun queryPurchasesHorizon( if (result.responseCode == BillingClient.BillingResponseCode.OK) { val mapped = purchaseList?.map { - android.util.Log.d("HORIZON_QUERY", " - Purchase: productIds=${it.products}") OpenIapLog.d(" - Purchase: productIds=${it.products}", TAG) it.toPurchase() } ?: emptyList() - android.util.Log.i("HORIZON_QUERY", "queryPurchasesHorizon: Returning ${mapped.size} mapped purchases") + OpenIapLog.d("queryPurchasesHorizon: Returning ${mapped.size} mapped purchases", TAG) continuation.resume(mapped) } else { - android.util.Log.w("HORIZON_QUERY", "queryPurchasesHorizon: Failed with code=${result.responseCode}") OpenIapLog.w("queryPurchasesHorizon: Failed with code=${result.responseCode}", TAG) continuation.resume(emptyList()) } 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 58192905..2e95f006 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 @@ -5152,12 +5152,12 @@ public sealed interface VerifyPurchaseResult { public interface MutationResolver { /** * Acknowledge a non-consumable purchase. Required within 3 days or Google auto-refunds. - * See: https://www.openiap.dev/docs/apis/android/acknowledge-purchase-android + * See: https://openiap.dev/docs/apis/android/acknowledge-purchase-android */ suspend fun acknowledgePurchaseAndroid(purchaseToken: String): Boolean /** * Present the refund request sheet (iOS 15+). See also Features → Refund. - * See: https://www.openiap.dev/docs/apis/ios/begin-refund-request-ios + * See: https://openiap.dev/docs/apis/ios/begin-refund-request-ios */ suspend fun beginRefundRequestIOS(sku: String): String? /** @@ -5165,17 +5165,17 @@ public interface MutationResolver { * * Returns true if available, false otherwise. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/check-alternative-billing-availability-android + * See: https://openiap.dev/docs/apis/android/check-alternative-billing-availability-android */ suspend fun checkAlternativeBillingAvailabilityAndroid(): Boolean /** * Clear pending transactions in the queue (sandbox helper). - * See: https://www.openiap.dev/docs/apis/ios/clear-transaction-ios + * See: https://openiap.dev/docs/apis/ios/clear-transaction-ios */ suspend fun clearTransactionIOS(): Boolean /** * Consume a consumable purchase so it can be re-bought. - * See: https://www.openiap.dev/docs/apis/android/consume-purchase-android + * See: https://openiap.dev/docs/apis/android/consume-purchase-android */ suspend fun consumePurchaseAndroid(purchaseToken: String): Boolean /** @@ -5185,7 +5185,7 @@ public interface MutationResolver { * * Returns token string, or null if creation failed. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/create-alternative-billing-token-android + * See: https://openiap.dev/docs/apis/android/create-alternative-billing-token-android */ suspend fun createAlternativeBillingTokenAndroid(): String? /** @@ -5194,27 +5194,27 @@ public interface MutationResolver { * * Returns external transaction token needed for reporting external transactions. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/create-billing-program-reporting-details-android + * See: https://openiap.dev/docs/apis/android/create-billing-program-reporting-details-android */ suspend fun createBillingProgramReportingDetailsAndroid(program: BillingProgramAndroid): BillingProgramReportingDetailsAndroid /** * Open the platform's subscription management UI. - * See: https://www.openiap.dev/docs/apis/deep-link-to-subscriptions + * See: https://openiap.dev/docs/apis/deep-link-to-subscriptions */ suspend fun deepLinkToSubscriptions(options: DeepLinkOptions? = null): Unit /** * Close the store connection and release resources. - * See: https://www.openiap.dev/docs/apis/end-connection + * See: https://openiap.dev/docs/apis/end-connection */ suspend fun endConnection(): Boolean /** * Complete a transaction after server-side verification. Required on Android within 3 days. - * See: https://www.openiap.dev/docs/apis/finish-transaction + * See: https://openiap.dev/docs/apis/finish-transaction */ suspend fun finishTransaction(purchase: PurchaseInput, isConsumable: Boolean? = null): Unit /** * Initialize the store connection. Call before any IAP API. - * See: https://www.openiap.dev/docs/apis/init-connection + * See: https://openiap.dev/docs/apis/init-connection */ suspend fun initConnection(config: InitConnectionConfig? = null): Boolean /** @@ -5224,7 +5224,7 @@ public interface MutationResolver { * Available in Google Play Billing Library 8.2.0+. * Returns availability result with isAvailable flag. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/is-billing-program-available-android + * See: https://openiap.dev/docs/apis/android/is-billing-program-available-android */ suspend fun isBillingProgramAvailableAndroid(program: BillingProgramAndroid): BillingProgramAvailabilityResultAndroid /** @@ -5233,29 +5233,29 @@ public interface MutationResolver { * * Shows Play Store dialog and optionally launches external URL. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/launch-external-link-android + * See: https://openiap.dev/docs/apis/android/launch-external-link-android */ suspend fun launchExternalLinkAndroid(params: LaunchExternalLinkParamsAndroid): Boolean /** * Show the App Store offer code redemption sheet. - * See: https://www.openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ suspend fun presentCodeRedemptionSheetIOS(): Boolean /** * Present an external purchase link, StoreKit External (iOS 16+). - * See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-link-ios + * See: https://openiap.dev/docs/apis/ios/present-external-purchase-link-ios */ suspend fun presentExternalPurchaseLinkIOS(url: String): ExternalPurchaseLinkResultIOS /** * Present the external purchase notice sheet (iOS 17.4+). * Uses ExternalPurchase.presentNoticeSheet() which returns a token when the user continues. * Reference: https://developer.apple.com/documentation/storekit/externalpurchase/presentnoticesheet() - * See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios + * See: https://openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios */ suspend fun presentExternalPurchaseNoticeSheetIOS(): ExternalPurchaseNoticeResultIOS /** * Initiate a purchase or subscription flow; rely on events for final state. - * See: https://www.openiap.dev/docs/apis/request-purchase + * See: https://openiap.dev/docs/apis/request-purchase */ suspend fun requestPurchase(params: RequestPurchaseProps): RequestPurchaseResult? /** @@ -5264,12 +5264,12 @@ public interface MutationResolver { * @deprecated Use promotedProductListenerIOS to receive the productId, * then call requestPurchase with that SKU instead. In StoreKit 2, * promoted products can be purchased directly via the standard purchase flow. - * See: https://www.openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios + * See: https://openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios */ suspend fun requestPurchaseOnPromotedProductIOS(): Boolean /** * Restore non-consumable and active subscription purchases. - * See: https://www.openiap.dev/docs/apis/restore-purchases + * See: https://openiap.dev/docs/apis/restore-purchases */ suspend fun restorePurchases(): Unit /** @@ -5278,29 +5278,29 @@ public interface MutationResolver { * * Returns true if user accepted, false if user canceled. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/show-alternative-billing-dialog-android + * See: https://openiap.dev/docs/apis/android/show-alternative-billing-dialog-android */ suspend fun showAlternativeBillingDialogAndroid(): Boolean /** * Present the disclosure sheet required before linking out via ExternalPurchaseCustomLink (iOS 18.1+). * Call this after a deliberate customer interaction before linking out to external purchases. * Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/shownotice(type:) - * See: https://www.openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios + * See: https://openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios */ suspend fun showExternalPurchaseCustomLinkNoticeIOS(noticeType: ExternalPurchaseCustomLinkNoticeTypeIOS): ExternalPurchaseCustomLinkNoticeResultIOS /** * Present the manage-subscriptions sheet and return changed purchases (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/show-manage-subscriptions-ios + * See: https://openiap.dev/docs/apis/ios/show-manage-subscriptions-ios */ suspend fun showManageSubscriptionsIOS(): List /** * Force sync transactions with the App Store (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/sync-ios + * See: https://openiap.dev/docs/apis/ios/sync-ios */ suspend fun syncIOS(): Boolean /** * Deprecated. Validate purchase receipts with the configured providers — use verifyPurchase instead. - * See: https://www.openiap.dev/docs/features/validation#verify-purchase + * See: https://openiap.dev/docs/features/validation#verify-purchase */ suspend fun validateReceipt(options: VerifyPurchaseProps): VerifyPurchaseResult /** @@ -5309,14 +5309,14 @@ public interface MutationResolver { * + receipt/JWS metadata, VerifyPurchaseResultAndroid carries Play Store * receipt fields (no isValid), and VerifyPurchaseResultHorizon uses success. * Inspect the concrete variant before reading fields. - * See: https://www.openiap.dev/docs/features/validation#verify-purchase + * See: https://openiap.dev/docs/features/validation#verify-purchase */ suspend fun verifyPurchase(options: VerifyPurchaseProps): VerifyPurchaseResult /** * Verify via a managed provider without standing up your own server. The * PurchaseVerificationProvider enum currently exposes only IAPKit; platform * availability may differ by implementation. - * See: https://www.openiap.dev/docs/features/validation#verify-purchase-with-provider + * See: https://openiap.dev/docs/features/validation#verify-purchase-with-provider */ suspend fun verifyPurchaseWithProvider(options: VerifyPurchaseWithProviderProps): VerifyPurchaseWithProviderResult } @@ -5328,22 +5328,22 @@ public interface QueryResolver { /** * Check eligibility for the external purchase notice sheet (iOS 17.4+). * Uses ExternalPurchase.canPresent. - * See: https://www.openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios + * See: https://openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios */ suspend fun canPresentExternalPurchaseNoticeIOS(): Boolean /** * Get the user's current entitlement for a product, using StoreKit 2 (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/current-entitlement-ios + * See: https://openiap.dev/docs/apis/ios/current-entitlement-ios */ suspend fun currentEntitlementIOS(sku: String): PurchaseIOS? /** * Fetch products or subscriptions from the store. - * See: https://www.openiap.dev/docs/apis/fetch-products + * See: https://openiap.dev/docs/apis/fetch-products */ suspend fun fetchProducts(params: ProductRequest): FetchProductsResult /** * Get details of all currently active subscriptions (filters by subscriptionIds when provided). - * See: https://www.openiap.dev/docs/apis/get-active-subscriptions + * See: https://openiap.dev/docs/apis/get-active-subscriptions */ suspend fun getActiveSubscriptions(subscriptionIds: List? = null): List /** @@ -5351,91 +5351,91 @@ public interface QueryResolver { * Requires the SK2ConsumableTransactionHistory Info.plist key in the host app * for finished consumables to be included (iOS 18+). * Unlike getAvailablePurchases, always returns the iOS-specific PurchaseIOS shape. - * See: https://www.openiap.dev/docs/apis/ios/get-all-transactions-ios + * See: https://openiap.dev/docs/apis/ios/get-all-transactions-ios */ suspend fun getAllTransactionsIOS(): List /** * Fetch the app transaction (iOS 16+). - * See: https://www.openiap.dev/docs/apis/ios/get-app-transaction-ios + * See: https://openiap.dev/docs/apis/ios/get-app-transaction-ios */ suspend fun getAppTransactionIOS(): AppTransaction? /** * List active purchases for the current user. - * See: https://www.openiap.dev/docs/apis/get-available-purchases + * See: https://openiap.dev/docs/apis/get-available-purchases */ suspend fun getAvailablePurchases(options: PurchaseOptions? = null): List /** * Fetch a token for Apple's External Purchase Server reporting API (iOS 18.1+). * Use this token to report transactions made through ExternalPurchaseCustomLink. * Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) - * See: https://www.openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios + * See: https://openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios */ suspend fun getExternalPurchaseCustomLinkTokenIOS(tokenType: ExternalPurchaseCustomLinkTokenTypeIOS): ExternalPurchaseCustomLinkTokenResultIOS /** * List unfinished StoreKit transactions in the queue. - * See: https://www.openiap.dev/docs/apis/ios/get-pending-transactions-ios + * See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios */ suspend fun getPendingTransactionsIOS(): List /** * Read the App Store-promoted product, if any (iOS 11+). - * See: https://www.openiap.dev/docs/apis/ios/get-promoted-product-ios + * See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios */ suspend fun getPromotedProductIOS(): ProductIOS? /** * Get base64-encoded receipt data (legacy validation). - * See: https://www.openiap.dev/docs/apis/ios/get-receipt-data-ios + * See: https://openiap.dev/docs/apis/ios/get-receipt-data-ios */ suspend fun getReceiptDataIOS(): String? /** * Return the user's storefront country code. - * See: https://www.openiap.dev/docs/apis/get-storefront + * See: https://openiap.dev/docs/apis/get-storefront */ suspend fun getStorefront(): String /** * Deprecated. Get the current App Store storefront country code — use cross-platform getStorefront instead. - * See: https://www.openiap.dev/docs/apis/ios/get-storefront-ios + * See: https://openiap.dev/docs/apis/ios/get-storefront-ios */ suspend fun getStorefrontIOS(): String /** * Return the JWS string for a transaction (StoreKit 2). - * See: https://www.openiap.dev/docs/apis/ios/get-transaction-jws-ios + * See: https://openiap.dev/docs/apis/ios/get-transaction-jws-ios */ suspend fun getTransactionJwsIOS(sku: String): String? /** * Check whether the user has any active subscription. - * See: https://www.openiap.dev/docs/apis/has-active-subscriptions + * See: https://openiap.dev/docs/apis/has-active-subscriptions */ suspend fun hasActiveSubscriptions(subscriptionIds: List? = null): Boolean /** * Check eligibility for the custom-link variant of external purchase (iOS 18.1+). * Returns true if the app can use custom external purchase links. * Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/iseligible - * See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios + * See: https://openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios */ suspend fun isEligibleForExternalPurchaseCustomLinkIOS(): Boolean /** * Check intro-offer eligibility for a subscription group. - * See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios + * See: https://openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios */ suspend fun isEligibleForIntroOfferIOS(groupID: String): Boolean /** * Check whether a transaction's JWS verification passed (StoreKit 2). - * See: https://www.openiap.dev/docs/apis/ios/is-transaction-verified-ios + * See: https://openiap.dev/docs/apis/ios/is-transaction-verified-ios */ suspend fun isTransactionVerifiedIOS(sku: String): Boolean /** * Get the latest verified transaction for a product, using StoreKit 2. - * See: https://www.openiap.dev/docs/apis/ios/latest-transaction-ios + * See: https://openiap.dev/docs/apis/ios/latest-transaction-ios */ suspend fun latestTransactionIOS(sku: String): PurchaseIOS? /** * Get subscription status objects from StoreKit 2 (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/subscription-status-ios + * See: https://openiap.dev/docs/apis/ios/subscription-status-ios */ suspend fun subscriptionStatusIOS(sku: String): List /** * Deprecated. Legacy App Store receipt validation — use verifyPurchase instead. - * See: https://www.openiap.dev/docs/apis/ios/validate-receipt-ios + * See: https://openiap.dev/docs/apis/ios/validate-receipt-ios */ suspend fun validateReceiptIOS(options: VerifyPurchaseProps): VerifyPurchaseResultIOS } 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 4cddc759..927063a8 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 @@ -62,7 +62,7 @@ import kotlinx.coroutines.launch */ class OpenIapStore(private val module: OpenIapProtocol) { init { - android.util.Log.i("OpenIapStore", "Initialized with module: ${module.javaClass.simpleName}") + OpenIapLog.i("Initialized with module: ${module.javaClass.simpleName}", "OpenIapStore") } constructor(context: Context) : this(buildModule(context, null, null)) @@ -116,30 +116,29 @@ class OpenIapStore(private val module: OpenIapProtocol) { // This ensures the purchase list reflects the new purchase immediately storeScope.launch { try { - android.util.Log.i("OpenIapStore", "Purchase update received, refreshing available purchases") + OpenIapLog.i("Purchase update received, refreshing available purchases", "OpenIapStore") // Wait a bit for the purchase to be fully processed by Horizon kotlinx.coroutines.delay(500) // Ensure connection is ready if (!isConnected.value) { - android.util.Log.w("OpenIapStore", "Not connected, skipping purchase refresh (connection will be restored on next app start)") + OpenIapLog.w("Not connected, skipping purchase refresh (connection will be restored on next app start)", "OpenIapStore") // Don't attempt to reconnect here as it may cause issues // The purchase will be available on next app launch return@launch } - android.util.Log.i("OpenIapStore", "About to call module.getAvailablePurchases(null)") + OpenIapLog.i("About to call module.getAvailablePurchases(null)", "OpenIapStore") val result = module.getAvailablePurchases(null) - android.util.Log.i("OpenIapStore", "module.getAvailablePurchases returned: ${result.size} purchases") + OpenIapLog.i("module.getAvailablePurchases returned: ${result.size} purchases", "OpenIapStore") result.forEachIndexed { index, purchase -> - android.util.Log.i("OpenIapStore", " Purchase[$index]: ${purchase.productId}") + OpenIapLog.i(" Purchase[$index]: ${purchase.productId}", "OpenIapStore") } _availablePurchases.value = result - android.util.Log.i("OpenIapStore", "Available purchases updated: ${result.size} purchases") + OpenIapLog.i("Available purchases updated: ${result.size} purchases", "OpenIapStore") } catch (e: Exception) { - android.util.Log.e("OpenIapStore", "Failed to refresh purchases after update", e) - e.printStackTrace() + OpenIapLog.e("Failed to refresh purchases after update", e, "OpenIapStore") } } } @@ -228,7 +227,7 @@ class OpenIapStore(private val module: OpenIapProtocol) { * @throws OpenIapError.InitConnection when the billing client fails to initialize * (e.g. Play Store missing, version too old). * - * @see init-connection + * @see init-connection */ val initConnection: MutationInitConnectionHandler = { config -> setLoading { it.initConnection = true } @@ -256,7 +255,7 @@ class OpenIapStore(private val module: OpenIapProtocol) { * @throws OpenIapError.InitConnection when the billing client fails to initialize * (e.g. Play Store missing, version too old). * - * @see init-connection + * @see init-connection */ suspend fun initConnection(): Boolean { OpenIapLog.i("OpenIapStore.initConnection(): Calling initConnection(null)...", "OpenIapStore") @@ -266,7 +265,7 @@ class OpenIapStore(private val module: OpenIapProtocol) { /** * Close the store connection and release resources. * - * @see https://www.openiap.dev/docs/apis/end-connection + * @see https://openiap.dev/docs/apis/end-connection */ val endConnection: MutationEndConnectionHandler = { removePurchaseUpdateListener(purchaseUpdateListener) @@ -296,15 +295,15 @@ class OpenIapStore(private val module: OpenIapProtocol) { * `Subscriptions` for Subs, mixed list for All. * @throws OpenIapError on store rejection (unknown SKU, network failure, not connected). * - * @see fetch-products + * @see fetch-products */ val fetchProducts: QueryFetchProductsHandler = { request -> - android.util.Log.i("OpenIapStore", "fetchProducts called with SKUs: ${request.skus}, type: ${request.type}") + OpenIapLog.i("fetchProducts called with SKUs: ${request.skus}, type: ${request.type}", "OpenIapStore") setLoading { it.fetchProducts = true } try { - android.util.Log.i("OpenIapStore", "Calling module.fetchProducts") + OpenIapLog.i("Calling module.fetchProducts", "OpenIapStore") val result = module.fetchProducts(request) - android.util.Log.i("OpenIapStore", "module.fetchProducts returned: $result") + OpenIapLog.i("module.fetchProducts returned: $result", "OpenIapStore") when (result) { is FetchProductsResultProducts -> { // Merge new products with existing ones @@ -390,19 +389,19 @@ class OpenIapStore(private val module: OpenIapProtocol) { * @return List of [Purchase] currently owned according to Play Billing. * @throws OpenIapError when the Play Billing query fails. * - * @see get-available-purchases + * @see get-available-purchases */ val getAvailablePurchases: QueryGetAvailablePurchasesHandler = { options -> - android.util.Log.i("OpenIapStore", "getAvailablePurchases called, module type: ${module.javaClass.simpleName}") + OpenIapLog.i("getAvailablePurchases called, module type: ${module.javaClass.simpleName}", "OpenIapStore") setLoading { it.restorePurchases = true } try { - android.util.Log.i("OpenIapStore", "Calling module.getAvailablePurchases(options)") + OpenIapLog.i("Calling module.getAvailablePurchases(options)", "OpenIapStore") val result = module.getAvailablePurchases(options) - android.util.Log.i("OpenIapStore", "module.getAvailablePurchases returned ${result.size} purchases") + OpenIapLog.i("module.getAvailablePurchases returned ${result.size} purchases", "OpenIapStore") _availablePurchases.value = result result } catch (e: Exception) { - android.util.Log.e("OpenIapStore", "getAvailablePurchases exception: ${e.message}", e) + OpenIapLog.e("getAvailablePurchases exception: ${e.message}", e, "OpenIapStore") setError(e.message) throw e } finally { @@ -428,7 +427,7 @@ class OpenIapStore(private val module: OpenIapProtocol) { * (or `OpenIapStore.currentPurchase` and `OpenIapStore.status.lastError` flows) for the * final state — there is no `currentError` field; errors live on `status.lastError`. * - * @see request-purchase + * @see request-purchase */ val requestPurchase: MutationRequestPurchaseHandler = { props -> val skuForStatus = when (val request = props.request) { @@ -460,7 +459,7 @@ class OpenIapStore(private val module: OpenIapProtocol) { * * Important: Google auto-refunds Android purchases NOT acknowledged/consumed within 3 days. * - * @see finish-transaction + * @see finish-transaction */ val finishTransaction: MutationFinishTransactionHandler = { purchaseInput, isConsumable -> val token = purchaseInput.purchaseToken @@ -483,7 +482,7 @@ class OpenIapStore(private val module: OpenIapProtocol) { /** * Get details of all currently active subscriptions. * - * @see https://www.openiap.dev/docs/apis/get-active-subscriptions + * @see https://openiap.dev/docs/apis/get-active-subscriptions */ suspend fun getActiveSubscriptions(subscriptionIds: List? = null): List = module.queryHandlers.getActiveSubscriptions?.invoke(subscriptionIds) ?: emptyList() @@ -491,7 +490,7 @@ class OpenIapStore(private val module: OpenIapProtocol) { /** * Check whether the user has any active subscription. * - * @see https://www.openiap.dev/docs/apis/has-active-subscriptions + * @see https://openiap.dev/docs/apis/has-active-subscriptions */ suspend fun hasActiveSubscriptions(subscriptionIds: List? = null): Boolean = module.queryHandlers.hasActiveSubscriptions?.invoke(subscriptionIds) ?: false @@ -499,7 +498,7 @@ class OpenIapStore(private val module: OpenIapProtocol) { /** * Open the platform's subscription management UI. * - * @see https://www.openiap.dev/docs/apis/deep-link-to-subscriptions + * @see https://openiap.dev/docs/apis/deep-link-to-subscriptions */ suspend fun deepLinkToSubscriptions(options: DeepLinkOptions) = module.mutationHandlers.deepLinkToSubscriptions?.invoke(options) @@ -509,7 +508,7 @@ class OpenIapStore(private val module: OpenIapProtocol) { /** * Check whether alternative billing is available for the user. * - * @see https://www.openiap.dev/docs/apis/android/check-alternative-billing-availability-android + * @see https://openiap.dev/docs/apis/android/check-alternative-billing-availability-android */ @Deprecated("Use isBillingProgramAvailable with BillingProgramAndroid.ExternalOffer instead") @Suppress("DEPRECATION") @@ -518,7 +517,7 @@ class OpenIapStore(private val module: OpenIapProtocol) { /** * Display Google's alternative billing information dialog. * - * @see https://www.openiap.dev/docs/apis/android/show-alternative-billing-dialog-android + * @see https://openiap.dev/docs/apis/android/show-alternative-billing-dialog-android */ @Deprecated("Use launchExternalLink instead") @Suppress("DEPRECATION") @@ -528,7 +527,7 @@ class OpenIapStore(private val module: OpenIapProtocol) { /** * Create a reporting token for an alternative billing flow. * - * @see https://www.openiap.dev/docs/apis/android/create-alternative-billing-token-android + * @see https://openiap.dev/docs/apis/android/create-alternative-billing-token-android */ @Deprecated("Use createBillingProgramReportingDetails with BillingProgramAndroid.ExternalOffer instead") @Suppress("DEPRECATION") @@ -541,7 +540,7 @@ class OpenIapStore(private val module: OpenIapProtocol) { /** * Check whether a billing program (e.g., External Payments) is available. * - * @see https://www.openiap.dev/docs/apis/android/is-billing-program-available-android + * @see https://openiap.dev/docs/apis/android/is-billing-program-available-android */ suspend fun isBillingProgramAvailable(program: BillingProgramAndroid): BillingProgramAvailabilityResultAndroid = module.isBillingProgramAvailable(program) @@ -549,7 +548,7 @@ class OpenIapStore(private val module: OpenIapProtocol) { /** * Create the reporting payload Google requires after a Developer-Provided Billing transaction (Play Billing 8.3.0+). * - * @see https://www.openiap.dev/docs/apis/android/create-billing-program-reporting-details-android + * @see https://openiap.dev/docs/apis/android/create-billing-program-reporting-details-android */ suspend fun createBillingProgramReportingDetails(program: BillingProgramAndroid): BillingProgramReportingDetailsAndroid = module.createBillingProgramReportingDetails(program) @@ -557,7 +556,7 @@ class OpenIapStore(private val module: OpenIapProtocol) { /** * Launch an external content/offer link from inside the Billing Programs flow (Play Billing 8.2.0+). * - * @see https://www.openiap.dev/docs/apis/android/launch-external-link-android + * @see https://openiap.dev/docs/apis/android/launch-external-link-android */ suspend fun launchExternalLink(activity: Activity, params: LaunchExternalLinkParamsAndroid): Boolean = module.launchExternalLink(activity, params) @@ -733,16 +732,15 @@ private fun buildModule(context: Context, store: String?, appId: String?): OpenI val defaultStore = try { val buildConfig = Class.forName("io.github.hyochan.openiap.BuildConfig") val storeValue = buildConfig.getField("OPENIAP_STORE").get(null) as? String ?: "play" - android.util.Log.i("OpenIapStore", "BuildConfig.OPENIAP_STORE = $storeValue") + OpenIapLog.i("BuildConfig.OPENIAP_STORE = $storeValue", "OpenIapStore") storeValue } catch (e: Throwable) { - android.util.Log.w("OpenIapStore", "Failed to read BuildConfig.OPENIAP_STORE: ${e.message}") + OpenIapLog.w("Failed to read BuildConfig.OPENIAP_STORE: ${e.message}", "OpenIapStore") "play" } val selected = (store ?: defaultStore).lowercase() - android.util.Log.i("OpenIapStore", "buildModule: selected=$selected, defaultStore=$defaultStore") OpenIapLog.d("buildModule: selected=$selected, defaultStore=$defaultStore", "OpenIapStore") return when (selected) { diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt index 83d7685d..cd059dbb 100644 --- a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt @@ -846,7 +846,7 @@ class OpenIapModule( OpenIapLog.d("", TAG) OpenIapLog.d("Required implementation:", TAG) OpenIapLog.d("1. Process payment through YOUR alternative payment system", TAG) - OpenIapLog.d("2. After successful payment, send this token to your backend:", TAG) + OpenIapLog.d("2. After successful payment, send this token to your backend", TAG) OpenIapLog.d(" Token: $tokenResult", TAG) OpenIapLog.d("3. Backend reports to Google Play Developer API within 24 hours:", TAG) OpenIapLog.d(" POST https://androidpublisher.googleapis.com/androidpublisher/v3/", TAG) @@ -857,15 +857,6 @@ class OpenIapModule( OpenIapLog.d("============================================================", TAG) OpenIapLog.d("=== END ALTERNATIVE BILLING ONLY MODE ===", TAG) - // TODO: In production, emit this token via callback for payment processing - // alternativeBillingCallback?.onTokenCreated( - // token = tokenResult, - // productId = props.skus.first(), - // onPaymentComplete = { transactionId -> - // // App reports to backend after payment success - // } - // ) - // Return empty list - app should handle purchase via alternative billing return@withContext emptyList() } else { @@ -962,9 +953,9 @@ class OpenIapModule( if (androidArgs.type == ProductQueryType.Subs) { val availableOffers = productDetails.subscriptionOfferDetails?.map { - "${it.basePlanId}:${it.offerToken}" + it.basePlanId } ?: emptyList() - OpenIapLog.d("Available offers for ${productDetails.productId}: $availableOffers", TAG) + OpenIapLog.d("Available offer base plans for ${productDetails.productId}: $availableOffers", TAG) val availableTokens = productDetails.subscriptionOfferDetails?.map { it.offerToken } ?: emptyList() val fromQueue = requestedOffersBySku[productDetails.productId]?.let { queue -> @@ -976,7 +967,7 @@ class OpenIapModule( OpenIapLog.d("Resolved offer token for ${productDetails.productId}: $resolved", TAG) if (resolved.isNullOrEmpty() || (availableTokens.isNotEmpty() && !availableTokens.contains(resolved))) { - OpenIapLog.w("Invalid offer token: $resolved not in $availableTokens", TAG) + OpenIapLog.w("Invalid offer token: $resolved not in available offer tokens", TAG) val err = OpenIapError.SkuOfferMismatch for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } consumePurchaseCallback(Result.success(emptyList())) @@ -1011,7 +1002,7 @@ class OpenIapModule( } if (!availableTokens.contains(androidArgs.offerToken)) { - OpenIapLog.w("Invalid one-time offer token: ${androidArgs.offerToken} not in $availableTokens", TAG) + OpenIapLog.w("Invalid one-time offer token: ${androidArgs.offerToken} not in available offer tokens", TAG) val err = OpenIapError.SkuOfferMismatch for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } consumePurchaseCallback(Result.success(emptyList())) @@ -1050,7 +1041,7 @@ class OpenIapModule( if (androidArgs.type == ProductQueryType.Subs && !androidArgs.purchaseToken.isNullOrBlank()) { // This is a subscription upgrade/downgrade - do not set obfuscatedProfileId OpenIapLog.d("=== Subscription Upgrade Flow ===", TAG) - OpenIapLog.d(" - Old Token: ${androidArgs.purchaseToken.take(10)}...", TAG) + OpenIapLog.d(" - Old Token: ${androidArgs.purchaseToken}", TAG) OpenIapLog.d(" - Target SKUs: ${androidArgs.skus}", TAG) OpenIapLog.d(" - Replacement mode: ${androidArgs.replacementMode}", TAG) OpenIapLog.d(" - Product Details Count: ${paramsList.size}", TAG) @@ -1462,7 +1453,7 @@ class OpenIapModule( OpenIapLog.d("Mapping purchase products=${purchase.products} to type=$productType basePlanId=$basePlanId (cached=${cached != null})", TAG) purchase.toPurchase(productType, basePlanId) } - OpenIapLog.d("Mapped purchases=${gson.toJson(mapped)}", TAG) + OpenIapLog.d("Mapped purchases count=${mapped.size}", TAG) notifySuspendedSubscriptions(mapped) for (converted in mapped) { for (listener in purchaseUpdateListeners) { @@ -1575,8 +1566,7 @@ class OpenIapModule( OpenIapLog.w("Failed to extract user choice details", TAG) } } catch (e: Exception) { - OpenIapLog.w("Error processing user choice details: ${e.message}", TAG) - e.printStackTrace() + OpenIapLog.e("Error processing user choice details", e, TAG) } OpenIapLog.d("==========================================", TAG) } @@ -1862,8 +1852,7 @@ class OpenIapModule( OpenIapLog.w("Failed to extract external transaction token", TAG) } } catch (e: Exception) { - OpenIapLog.w("Error processing developer billing details: ${e.message}", TAG) - e.printStackTrace() + OpenIapLog.e("Error processing developer billing details", e, TAG) } OpenIapLog.d("==========================================", TAG) } 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 8beb7a94..d9d691e3 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 @@ -327,6 +327,7 @@ class OpenIapErrorTest { } @Test + @Suppress("DEPRECATION") fun `fromBillingResponseCode forwards debugMessage for every response code`() { val debug = "offerToken does not match any product details" val codesToAssert = listOf( diff --git a/packages/google/package.json b/packages/google/package.json index 3ca23430..6e3c4213 100644 --- a/packages/google/package.json +++ b/packages/google/package.json @@ -1,6 +1,6 @@ { "name": "@hyodotdev/openiap-android", - "version": "1.2.12", + "version": "2.1.5", "private": true, "description": "OpenIAP Android/Kotlin implementation", "scripts": { @@ -12,5 +12,5 @@ "dependencies": { "@hyodotdev/openiap-gql": "workspace:*" }, - "packageManager": "bun@1.1.0" + "packageManager": "bun@1.3.13" } diff --git a/packages/google/scripts/open-android-studio.sh b/packages/google/scripts/open-android-studio.sh index d4cd21a0..ab713409 100755 --- a/packages/google/scripts/open-android-studio.sh +++ b/packages/google/scripts/open-android-studio.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -euo pipefail # OpenIAP Google - Open in Android Studio # This script opens the Google (Android) package in Android Studio @@ -39,4 +40,4 @@ echo "📱 Running from Terminal:" echo " ./gradlew :Example:installDebug && adb shell am start -n dev.hyo.martie/.MainActivity" echo "" echo "🔍 View Logs:" -echo " adb logcat -s OpenIAP:V MainActivity:V" \ No newline at end of file +echo " adb logcat -s OpenIAP:V MainActivity:V" diff --git a/packages/google/scripts/publish-local.sh b/packages/google/scripts/publish-local.sh index f556b314..712e5714 100755 --- a/packages/google/scripts/publish-local.sh +++ b/packages/google/scripts/publish-local.sh @@ -38,10 +38,6 @@ signingInMemoryKeyFile="$(read_prop signingInMemoryKeyFile)" signingInMemoryKey="$(read_prop signingInMemoryKey)" openIapVersion="$(read_prop openIapVersion)" openIapGroupId="$(read_prop OPENIAP_GROUP_ID)" -# Staging profile id (either key is accepted) -sonatypeStagingProfileId="$(read_prop sonatypeStagingProfileId)" -mavenCentralStagingProfileId="$(read_prop mavenCentralStagingProfileId)" -sonatypeHostProp="$(read_prop sonatypeHost)" if [[ -z "$mavenCentralUsername" || -z "$mavenCentralPassword" ]]; then echo "Missing required keys in local.properties. Required: mavenCentralUsername, mavenCentralPassword" @@ -85,27 +81,11 @@ if [[ -n "$openIapVersion" ]]; then export ORG_GRADLE_PROJECT_openIapVersion="$openIapVersion" fi -# Optional: select Sonatype host via env SONATYPE_HOST=(S01|DEFAULT) -if [[ -n "${SONATYPE_HOST:-}" ]]; then - export ORG_GRADLE_PROJECT_sonatypeHost="$SONATYPE_HOST" -elif [[ -n "$sonatypeHostProp" ]]; then - # Allow specifying the host via local.properties (sonatypeHost=DEFAULT|S01) - export ORG_GRADLE_PROJECT_sonatypeHost="$sonatypeHostProp" -fi - # Optional: override Maven groupId via local.properties OPENIAP_GROUP_ID if [[ -n "$openIapGroupId" ]]; then export ORG_GRADLE_PROJECT_OPENIAP_GROUP_ID="$openIapGroupId" fi -# Optional: set explicit staging profile id to bypass lookup -if [[ -n "$sonatypeStagingProfileId" ]]; then - export ORG_GRADLE_PROJECT_mavenCentralStagingProfileId="$sonatypeStagingProfileId" -fi -if [[ -n "$mavenCentralStagingProfileId" ]]; then - export ORG_GRADLE_PROJECT_mavenCentralStagingProfileId="$mavenCentralStagingProfileId" -fi - # Optional first argument can be "local" to publish to Maven Local for testing MODE=${1:-central} @@ -116,31 +96,12 @@ if [[ "$MODE" == "local" ]]; then echo "Publishing to Maven Local (for local testing)..." ./gradlew :openiap:publishToMavenLocal --no-daemon --stacktrace echo "Published to Maven Local." - echo "Use dependency: ${openIapGroupId:-io.github.hyochan}:openiap-google:${openIapVersion:-}" + echo "Use dependency: ${openIapGroupId:-io.github.hyochan.openiap}:openiap-google:${openIapVersion:-}" exit 0 fi echo "Building and publishing to Maven Central..." - -# Try with configured host (default S01 from Gradle config). If it fails with -# a common stagingProfiles error, retry using the alternate host automatically. -set +e ./gradlew :openiap:publishAndReleaseToMavenCentral --no-daemon --no-parallel --stacktrace -rc=$? -set -e - -if [[ $rc -ne 0 ]]; then - echo "Initial publish failed (exit $rc). Attempting host fallback..." - currentHost="${ORG_GRADLE_PROJECT_sonatypeHost:-S01}" - if [[ "$currentHost" =~ ^(?i)s01$ ]]; then - fallbackHost="DEFAULT" - else - fallbackHost="S01" - fi - echo "Retrying with SONATYPE_HOST=$fallbackHost" - export ORG_GRADLE_PROJECT_sonatypeHost="$fallbackHost" - ./gradlew :openiap:publishAndReleaseToMavenCentral --no-daemon --no-parallel --stacktrace -fi echo "Publishing completed." echo "Check https://central.sonatype.com/publishing/deployments" diff --git a/packages/google/scripts/update-version.sh b/packages/google/scripts/update-version.sh index 01454d30..4d150fd0 100755 --- a/packages/google/scripts/update-version.sh +++ b/packages/google/scripts/update-version.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -# This script updates the version in README.md and openiap-versions.json +# This script updates the Google version in openiap-versions.json and synced metadata. # Usage: ./scripts/update-version.sh if [ $# -ne 1 ]; then @@ -15,41 +15,49 @@ VERSION="$1" VERSION="${VERSION#v}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -README_FILE="${REPO_ROOT}/README.md" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" VERSIONS_FILE="${REPO_ROOT}/openiap-versions.json" echo "Updating version to $VERSION" -# Update README.md -if [[ "$OSTYPE" == "darwin"* ]]; then - # macOS uses different sed syntax - sed -i '' "s/openiap-google:[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*/openiap-google:$VERSION/g" "$README_FILE" -else - # Linux - sed -i "s/openiap-google:[0-9]\+\.[0-9]\+\.[0-9]\+/openiap-google:$VERSION/g" "$README_FILE" +if [[ ! -f "$VERSIONS_FILE" ]]; then + echo "Error: openiap-versions.json not found at $VERSIONS_FILE" >&2 + exit 1 fi -# Update openiap-versions.json (preserving spec version) -if command -v python3 &> /dev/null; then - SPEC_VERSION=$(python3 -c "import json; print(json.load(open('$VERSIONS_FILE'))['spec'])" 2>/dev/null || echo "2.0.0") +# Update openiap-versions.json without dropping other version fields +if command -v jq &> /dev/null; then + tmp_file="${VERSIONS_FILE}.tmp" + jq --arg version "$VERSION" '.google = $version' "$VERSIONS_FILE" > "$tmp_file" + mv "$tmp_file" "$VERSIONS_FILE" +elif command -v python3 &> /dev/null; then + VERSION="$VERSION" VERSIONS_FILE="$VERSIONS_FILE" python3 - <<'PY' +import json +import os + +versions_file = os.environ["VERSIONS_FILE"] +with open(versions_file, "r", encoding="utf-8") as f: + data = json.load(f) +data["google"] = os.environ["VERSION"] +with open(versions_file, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + f.write("\n") +PY else - SPEC_VERSION=$(grep '"spec"' "$VERSIONS_FILE" | sed 's/.*"spec".*"\([^"]*\)".*/\1/') + echo "Error: jq or python3 is required to update openiap-versions.json" >&2 + exit 1 fi -cat > "$VERSIONS_FILE" << EOF -{ - "spec": "$SPEC_VERSION", - "google": "$VERSION" -} -EOF +"$REPO_ROOT/scripts/sync-versions.sh" -echo "✅ Updated README.md and openiap-versions.json to version $VERSION" +echo "✅ Updated openiap-versions.json to version $VERSION" echo "" echo "Files modified:" -echo " - $README_FILE" echo " - $VERSIONS_FILE" +echo " - $REPO_ROOT/packages/*/openiap-versions.json" +echo " - $REPO_ROOT/packages/{gql,docs,google,apple}/package.json" echo "" echo "To commit these changes:" -echo " git add README.md openiap-versions.json" -echo " git commit -m \"chore: update version to $VERSION\"" \ No newline at end of file +echo " git add openiap-versions.json packages/*/openiap-versions.json" +echo " git add packages/gql/package.json packages/docs/package.json packages/google/package.json packages/apple/package.json" +echo " git commit -m \"chore(google): update version to $VERSION\"" diff --git a/packages/gql/README.md b/packages/gql/README.md index 935554f6..8905a056 100644 --- a/packages/gql/README.md +++ b/packages/gql/README.md @@ -39,7 +39,7 @@ Generated outputs: Uses [`@graphql-codegen/cli`](https://www.the-guild.dev/graphql/codegen). 1. Ensure Node 18+ is installed. -2. Install dependencies once: `npm install` +2. Install dependencies once from the monorepo root: `bun install --frozen-lockfile` 3. Generate types: `bun run generate:ts` 4. Generated output: `src/generated/types.ts` diff --git a/packages/gql/codegen/core/parser.ts b/packages/gql/codegen/core/parser.ts index 48c64433..671fc5f4 100644 --- a/packages/gql/codegen/core/parser.ts +++ b/packages/gql/codegen/core/parser.ts @@ -124,7 +124,7 @@ export class SchemaParser { const trimmed = line.trim(); // Track current type context - const typeMatch = trimmed.match(/^type\s+([A-Za-z0-9_]+)/); + const typeMatch = trimmed.match(/^(?:extend\s+)?type\s+([A-Za-z0-9_]+)/); if (typeMatch) { currentTypeName = typeMatch[1]; if (expectUnionType) { diff --git a/packages/gql/codegen/core/schema-linter.ts b/packages/gql/codegen/core/schema-linter.ts index def52fc6..6423f7bf 100644 --- a/packages/gql/codegen/core/schema-linter.ts +++ b/packages/gql/codegen/core/schema-linter.ts @@ -24,6 +24,15 @@ export interface LintOptions { strict?: boolean; } +const PLATFORM_TYPE_SUFFIX_EXCEPTIONS = new Set([ + // Public API names kept for source/binary compatibility. + 'AppTransaction', + 'ProductAndroidOneTimePurchaseOfferDetail', + 'ProductSubscriptionAndroidOfferDetails', + 'UserChoiceBillingDetails', + 'VerifyPurchaseResultHorizon', +]); + /** * Lint schema conventions and return findings. */ @@ -51,7 +60,7 @@ export function lintSchema( const lineNum = i + 1; // Track type definitions - const typeMatch = trimmed.match(/^type\s+([A-Za-z0-9_]+)/); + const typeMatch = trimmed.match(/^(?:extend\s+)?type\s+([A-Za-z0-9_]+)/); if (typeMatch) { const typeName = typeMatch[1]; currentTypeName = typeName; @@ -63,23 +72,27 @@ export function lintSchema( // Platform suffix checks for types in platform-specific files if (isIOSFile && !typeName.endsWith('IOS') && !typeName.startsWith('Query') && !typeName.startsWith('Mutation')) { - results.push({ - level: 'warning', - file: fileName, - line: lineNum, - message: `Type "${typeName}" in iOS file should end with "IOS" suffix`, - rule: 'ios-type-suffix', - }); + 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 (isAndroidFile && !typeName.endsWith('Android') && !typeName.startsWith('Query') && !typeName.startsWith('Mutation')) { - results.push({ - level: 'warning', - file: fileName, - line: lineNum, - message: `Type "${typeName}" in Android file should end with "Android" suffix`, - rule: 'android-type-suffix', - }); + 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', + }); + } } continue; diff --git a/packages/gql/package-lock.json b/packages/gql/package-lock.json deleted file mode 100644 index 2820059b..00000000 --- a/packages/gql/package-lock.json +++ /dev/null @@ -1,4646 +0,0 @@ -{ - "name": "openiap-gql", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "openiap-gql", - "version": "1.0.0", - "license": "ISC", - "devDependencies": { - "@graphql-codegen/add": "^6.0.0", - "@graphql-codegen/cli": "^6.0.0", - "@graphql-codegen/typescript": "^5.0.0", - "graphql": "^16.11.0", - "ts-node": "^10.9.2", - "typescript": "^5.9.2" - } - }, - "node_modules/@ardatan/relay-compiler": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-12.0.3.tgz", - "integrity": "sha512-mBDFOGvAoVlWaWqs3hm1AciGHSQE1rqFc/liZTyYz/Oek9yZdT5H26pH2zAFuEiTiBVPPyMuqf5VjOFPI2DGsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/generator": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/runtime": "^7.26.10", - "chalk": "^4.0.0", - "fb-watchman": "^2.0.0", - "immutable": "~3.7.6", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "relay-runtime": "12.0.0", - "signedsource": "^1.0.0" - }, - "bin": { - "relay-compiler": "bin/relay-compiler" - }, - "peerDependencies": { - "graphql": "*" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", - "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.4" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", - "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@envelop/core": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@envelop/core/-/core-5.3.1.tgz", - "integrity": "sha512-n29V3vRqXvPcG76C8zE482LQykk0P66zv1mjpk7aHeGe9qnh8AzB/RvoX5SVFwApJQPp0ixob8NoYXg4FHKMGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@envelop/instrumentation": "^1.0.0", - "@envelop/types": "^5.2.1", - "@whatwg-node/promise-helpers": "^1.2.4", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@envelop/instrumentation": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@envelop/instrumentation/-/instrumentation-1.0.0.tgz", - "integrity": "sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@whatwg-node/promise-helpers": "^1.2.1", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@envelop/types": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@envelop/types/-/types-5.2.1.tgz", - "integrity": "sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@whatwg-node/promise-helpers": "^1.0.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@fastify/busboy": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", - "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@graphql-codegen/add": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-6.0.0.tgz", - "integrity": "sha512-biFdaURX0KTwEJPQ1wkT6BRgNasqgQ5KbCI1a3zwtLtO7XTo7/vKITPylmiU27K5DSOWYnY/1jfSqUAEBuhZrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-codegen/plugin-helpers": "^6.0.0", - "tslib": "~2.6.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/cli": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-6.0.0.tgz", - "integrity": "sha512-tvchLVCMtorDE+UwgQbrjyaQK16GCZA+QomTxZazRx64ixtgmbEiQV7GhCBy0y0Bo7/tcTJb6sy9G/TL/BgiOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/generator": "^7.18.13", - "@babel/template": "^7.18.10", - "@babel/types": "^7.18.13", - "@graphql-codegen/client-preset": "^5.0.0", - "@graphql-codegen/core": "^5.0.0", - "@graphql-codegen/plugin-helpers": "^6.0.0", - "@graphql-tools/apollo-engine-loader": "^8.0.0", - "@graphql-tools/code-file-loader": "^8.0.0", - "@graphql-tools/git-loader": "^8.0.0", - "@graphql-tools/github-loader": "^8.0.0", - "@graphql-tools/graphql-file-loader": "^8.0.0", - "@graphql-tools/json-file-loader": "^8.0.0", - "@graphql-tools/load": "^8.1.0", - "@graphql-tools/url-loader": "^8.0.0", - "@graphql-tools/utils": "^10.0.0", - "@inquirer/prompts": "^7.8.2", - "@whatwg-node/fetch": "^0.10.0", - "chalk": "^4.1.0", - "cosmiconfig": "^9.0.0", - "debounce": "^2.0.0", - "detect-indent": "^6.0.0", - "graphql-config": "^5.1.1", - "is-glob": "^4.0.1", - "jiti": "^2.3.0", - "json-to-pretty-yaml": "^1.2.2", - "listr2": "^9.0.0", - "log-symbols": "^4.0.0", - "micromatch": "^4.0.5", - "shell-quote": "^1.7.3", - "string-env-interpolation": "^1.0.1", - "ts-log": "^2.2.3", - "tslib": "^2.4.0", - "yaml": "^2.3.1", - "yargs": "^17.0.0" - }, - "bin": { - "gql-gen": "cjs/bin.js", - "graphql-code-generator": "cjs/bin.js", - "graphql-codegen": "cjs/bin.js", - "graphql-codegen-esm": "esm/bin.js" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "@parcel/watcher": "^2.1.0", - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - }, - "peerDependenciesMeta": { - "@parcel/watcher": { - "optional": true - } - } - }, - "node_modules/@graphql-codegen/client-preset": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-5.0.1.tgz", - "integrity": "sha512-3dXS7Sh/AkV+Ewq/HB1DSCb0tZBOIdTL8zkGQjRKWaf14x21h2f/xKl2zhRh6KlXjcCrIpX+AxHAhQxs6cXwVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/template": "^7.20.7", - "@graphql-codegen/add": "^6.0.0", - "@graphql-codegen/gql-tag-operations": "5.0.0", - "@graphql-codegen/plugin-helpers": "^6.0.0", - "@graphql-codegen/typed-document-node": "^6.0.0", - "@graphql-codegen/typescript": "^5.0.0", - "@graphql-codegen/typescript-operations": "^5.0.0", - "@graphql-codegen/visitor-plugin-common": "^6.0.0", - "@graphql-tools/documents": "^1.0.0", - "@graphql-tools/utils": "^10.0.0", - "@graphql-typed-document-node/core": "3.2.0", - "tslib": "~2.6.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", - "graphql-sock": "^1.0.0" - }, - "peerDependenciesMeta": { - "graphql-sock": { - "optional": true - } - } - }, - "node_modules/@graphql-codegen/core": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/core/-/core-5.0.0.tgz", - "integrity": "sha512-vLTEW0m8LbE4xgRwbFwCdYxVkJ1dBlVJbQyLb9Q7bHnVFgHAP982Xo8Uv7FuPBmON+2IbTjkCqhFLHVZbqpvjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-codegen/plugin-helpers": "^6.0.0", - "@graphql-tools/schema": "^10.0.0", - "@graphql-tools/utils": "^10.0.0", - "tslib": "~2.6.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/gql-tag-operations": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-5.0.0.tgz", - "integrity": "sha512-kC2pc/tyzVc1laZtlfuQHqYxF4UqB4YXzAboFfeY1cxrxCh/+H70jHnfA1O4vhPndiRd+XZA8wxPv0hIqDXYaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-codegen/plugin-helpers": "^6.0.0", - "@graphql-codegen/visitor-plugin-common": "6.0.0", - "@graphql-tools/utils": "^10.0.0", - "auto-bind": "~4.0.0", - "tslib": "~2.6.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/plugin-helpers": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-6.0.0.tgz", - "integrity": "sha512-Z7P89vViJvQakRyMbq/JF2iPLruRFOwOB6IXsuSvV/BptuuEd7fsGPuEf8bdjjDxUY0pJZnFN8oC7jIQ8p9GKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/utils": "^10.0.0", - "change-case-all": "1.0.15", - "common-tags": "1.8.2", - "import-from": "4.0.0", - "lodash": "~4.17.0", - "tslib": "~2.6.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/schema-ast": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/schema-ast/-/schema-ast-5.0.0.tgz", - "integrity": "sha512-jn7Q3PKQc0FxXjbpo9trxzlz/GSFQWxL042l0iC8iSbM/Ar+M7uyBwMtXPsev/3Razk+osQyreghIz0d2+6F7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-codegen/plugin-helpers": "^6.0.0", - "@graphql-tools/utils": "^10.0.0", - "tslib": "~2.6.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/typed-document-node": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-6.0.0.tgz", - "integrity": "sha512-OYmbadwvjq19yCZjioy901pLI9YV6i7A0fP3MpcJlo2uQVY27RJPcN2NeLfFzXdHr6f5bm9exqB6X1iKimfA2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-codegen/plugin-helpers": "^6.0.0", - "@graphql-codegen/visitor-plugin-common": "6.0.0", - "auto-bind": "~4.0.0", - "change-case-all": "1.0.15", - "tslib": "~2.6.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/typescript": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-5.0.0.tgz", - "integrity": "sha512-u90SGM6+Rdc3Je1EmVQOrGk5fl7hK1cLR4y5Q1MeUenj0aZFxKno65DCW7RcQpcfebvkPsVGA6y3oS02wPFj6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-codegen/plugin-helpers": "^6.0.0", - "@graphql-codegen/schema-ast": "^5.0.0", - "@graphql-codegen/visitor-plugin-common": "6.0.0", - "auto-bind": "~4.0.0", - "tslib": "~2.6.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/typescript-operations": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-5.0.0.tgz", - "integrity": "sha512-mqgp/lp5v7w+RYj5AJ/BVquP+sgje3EAgg++62ciolOB5zzWT8en09cRdNq4UZfszCYTOtlhCG7NQAAcSae37A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-codegen/plugin-helpers": "^6.0.0", - "@graphql-codegen/typescript": "^5.0.0", - "@graphql-codegen/visitor-plugin-common": "6.0.0", - "auto-bind": "~4.0.0", - "tslib": "~2.6.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", - "graphql-sock": "^1.0.0" - }, - "peerDependenciesMeta": { - "graphql-sock": { - "optional": true - } - } - }, - "node_modules/@graphql-codegen/visitor-plugin-common": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-6.0.0.tgz", - "integrity": "sha512-K05Jv2elOeFstH3i+Ah0Pi9do6NYUvrbdhEkP+UvP9fmIro1hCKwcIEP7j4VFz8mt3gAC3dB5KVJDoyaPUgi4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-codegen/plugin-helpers": "^6.0.0", - "@graphql-tools/optimize": "^2.0.0", - "@graphql-tools/relay-operation-optimizer": "^7.0.0", - "@graphql-tools/utils": "^10.0.0", - "auto-bind": "~4.0.0", - "change-case-all": "1.0.15", - "dependency-graph": "^1.0.0", - "graphql-tag": "^2.11.0", - "parse-filepath": "^1.0.2", - "tslib": "~2.6.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-hive/signal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@graphql-hive/signal/-/signal-1.0.0.tgz", - "integrity": "sha512-RiwLMc89lTjvyLEivZ/qxAC5nBHoS2CtsWFSOsN35sxG9zoo5Z+JsFHM8MlvmO9yt+MJNIyC5MLE1rsbOphlag==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@graphql-tools/apollo-engine-loader": { - "version": "8.0.22", - "resolved": "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-8.0.22.tgz", - "integrity": "sha512-ssD2wNxeOTRcUEkuGcp0KfZAGstL9YLTe/y3erTDZtOs2wL1TJESw8NVAp+3oUHPeHKBZQB4Z6RFEbPgMdT2wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/utils": "^10.9.1", - "@whatwg-node/fetch": "^0.10.0", - "sync-fetch": "0.6.0-2", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/batch-execute": { - "version": "9.0.19", - "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-9.0.19.tgz", - "integrity": "sha512-VGamgY4PLzSx48IHPoblRw0oTaBa7S26RpZXt0Y4NN90ytoE0LutlpB2484RbkfcTjv9wa64QD474+YP1kEgGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/utils": "^10.9.1", - "@whatwg-node/promise-helpers": "^1.3.0", - "dataloader": "^2.2.3", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/batch-execute/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@graphql-tools/code-file-loader": { - "version": "8.1.22", - "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-8.1.22.tgz", - "integrity": "sha512-FSka29kqFkfFmw36CwoQ+4iyhchxfEzPbXOi37lCEjWLHudGaPkXc3RyB9LdmBxx3g3GHEu43a5n5W8gfcrMdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/graphql-tag-pluck": "8.3.21", - "@graphql-tools/utils": "^10.9.1", - "globby": "^11.0.3", - "tslib": "^2.4.0", - "unixify": "^1.0.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/delegate": { - "version": "10.2.23", - "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-10.2.23.tgz", - "integrity": "sha512-xrPtl7f1LxS+B6o+W7ueuQh67CwRkfl+UKJncaslnqYdkxKmNBB4wnzVcW8ZsRdwbsla/v43PtwAvSlzxCzq2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/batch-execute": "^9.0.19", - "@graphql-tools/executor": "^1.4.9", - "@graphql-tools/schema": "^10.0.25", - "@graphql-tools/utils": "^10.9.1", - "@repeaterjs/repeater": "^3.0.6", - "@whatwg-node/promise-helpers": "^1.3.0", - "dataloader": "^2.2.3", - "dset": "^3.1.2", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/delegate/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@graphql-tools/documents": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/documents/-/documents-1.0.1.tgz", - "integrity": "sha512-aweoMH15wNJ8g7b2r4C4WRuJxZ0ca8HtNO54rkye/3duxTkW4fGBEutCx03jCIr5+a1l+4vFJNP859QnAVBVCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/executor": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.4.9.tgz", - "integrity": "sha512-SAUlDT70JAvXeqV87gGzvDzUGofn39nvaVcVhNf12Dt+GfWHtNNO/RCn/Ea4VJaSLGzraUd41ObnN3i80EBU7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/utils": "^10.9.1", - "@graphql-typed-document-node/core": "^3.2.0", - "@repeaterjs/repeater": "^3.0.4", - "@whatwg-node/disposablestack": "^0.0.6", - "@whatwg-node/promise-helpers": "^1.0.0", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/executor-common": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-common/-/executor-common-0.0.4.tgz", - "integrity": "sha512-SEH/OWR+sHbknqZyROCFHcRrbZeUAyjCsgpVWCRjqjqRbiJiXq6TxNIIOmpXgkrXWW/2Ev4Wms6YSGJXjdCs6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@envelop/core": "^5.2.3", - "@graphql-tools/utils": "^10.8.1" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/executor-graphql-ws": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-2.0.7.tgz", - "integrity": "sha512-J27za7sKF6RjhmvSOwOQFeNhNHyP4f4niqPnerJmq73OtLx9Y2PGOhkXOEB0PjhvPJceuttkD2O1yMgEkTGs3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/executor-common": "^0.0.6", - "@graphql-tools/utils": "^10.9.1", - "@whatwg-node/disposablestack": "^0.0.6", - "graphql-ws": "^6.0.6", - "isomorphic-ws": "^5.0.0", - "tslib": "^2.8.1", - "ws": "^8.18.3" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/executor-graphql-ws/node_modules/@graphql-tools/executor-common": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-common/-/executor-common-0.0.6.tgz", - "integrity": "sha512-JAH/R1zf77CSkpYATIJw+eOJwsbWocdDjY+avY7G+P5HCXxwQjAjWVkJI1QJBQYjPQDVxwf1fmTZlIN3VOadow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@envelop/core": "^5.3.0", - "@graphql-tools/utils": "^10.9.1" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/executor-graphql-ws/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@graphql-tools/executor-http": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-http/-/executor-http-1.3.3.tgz", - "integrity": "sha512-LIy+l08/Ivl8f8sMiHW2ebyck59JzyzO/yF9SFS4NH6MJZUezA1xThUXCDIKhHiD56h/gPojbkpcFvM2CbNE7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-hive/signal": "^1.0.0", - "@graphql-tools/executor-common": "^0.0.4", - "@graphql-tools/utils": "^10.8.1", - "@repeaterjs/repeater": "^3.0.4", - "@whatwg-node/disposablestack": "^0.0.6", - "@whatwg-node/fetch": "^0.10.4", - "@whatwg-node/promise-helpers": "^1.3.0", - "meros": "^1.2.1", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/executor-http/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@graphql-tools/executor-legacy-ws": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-1.1.19.tgz", - "integrity": "sha512-bEbv/SlEdhWQD0WZLUX1kOenEdVZk1yYtilrAWjRUgfHRZoEkY9s+oiqOxnth3z68wC2MWYx7ykkS5hhDamixg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/utils": "^10.9.1", - "@types/ws": "^8.0.0", - "isomorphic-ws": "^5.0.0", - "tslib": "^2.4.0", - "ws": "^8.17.1" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/git-loader": { - "version": "8.0.26", - "resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-8.0.26.tgz", - "integrity": "sha512-0g+9eng8DaT4ZmZvUmPgjLTgesUa6M8xrDjNBltRldZkB055rOeUgJiKmL6u8PjzI5VxkkVsn0wtAHXhDI2UXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/graphql-tag-pluck": "8.3.21", - "@graphql-tools/utils": "^10.9.1", - "is-glob": "4.0.3", - "micromatch": "^4.0.8", - "tslib": "^2.4.0", - "unixify": "^1.0.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/github-loader": { - "version": "8.0.22", - "resolved": "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-8.0.22.tgz", - "integrity": "sha512-uQ4JNcNPsyMkTIgzeSbsoT9hogLjYrZooLUYd173l5eUGUi49EAcsGdiBCKaKfEjanv410FE8hjaHr7fjSRkJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/executor-http": "^1.1.9", - "@graphql-tools/graphql-tag-pluck": "^8.3.21", - "@graphql-tools/utils": "^10.9.1", - "@whatwg-node/fetch": "^0.10.0", - "@whatwg-node/promise-helpers": "^1.0.0", - "sync-fetch": "0.6.0-2", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/graphql-file-loader": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.1.1.tgz", - "integrity": "sha512-5JaUE3zMHW21Oh3bGSNKcr/Mi6oZ9/QWlBCNYbGy+09U23EOZmhPn9a44zP3gXcnnj0C+YVEr8dsMaoaB3UVGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/import": "7.1.1", - "@graphql-tools/utils": "^10.9.1", - "globby": "^11.0.3", - "tslib": "^2.4.0", - "unixify": "^1.0.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/graphql-tag-pluck": { - "version": "8.3.21", - "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.3.21.tgz", - "integrity": "sha512-TJhELNvR1tmghXMi6HVKp/Swxbx1rcSp/zdkuJZT0DCM3vOY11FXY6NW3aoxumcuYDNN3jqXcCPKstYGFPi5GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/plugin-syntax-import-assertions": "^7.26.0", - "@babel/traverse": "^7.26.10", - "@babel/types": "^7.26.10", - "@graphql-tools/utils": "^10.9.1", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/import": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-7.1.1.tgz", - "integrity": "sha512-zhlhaUmeTfV76vMoLRn9xCVMVc7sLf10ve5GKEhXFFDcWA6+vEZGk9CCm1VlPf2kyKGlF7bwLVzfepb3ZoOU9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/utils": "^10.9.1", - "@theguild/federation-composition": "^0.19.0", - "resolve-from": "5.0.0", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/json-file-loader": { - "version": "8.0.20", - "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-8.0.20.tgz", - "integrity": "sha512-5v6W+ZLBBML5SgntuBDLsYoqUvwfNboAwL6BwPHi3z/hH1f8BS9/0+MCW9OGY712g7E4pc3y9KqS67mWF753eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/utils": "^10.9.1", - "globby": "^11.0.3", - "tslib": "^2.4.0", - "unixify": "^1.0.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/load": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-8.1.2.tgz", - "integrity": "sha512-WhDPv25/jRND+0uripofMX0IEwo6mrv+tJg6HifRmDu8USCD7nZhufT0PP7lIcuutqjIQFyogqT70BQsy6wOgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/schema": "^10.0.25", - "@graphql-tools/utils": "^10.9.1", - "p-limit": "3.1.0", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/merge": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.1.1.tgz", - "integrity": "sha512-BJ5/7Y7GOhTuvzzO5tSBFL4NGr7PVqTJY3KeIDlVTT8YLcTXtBR+hlrC3uyEym7Ragn+zyWdHeJ9ev+nRX1X2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/utils": "^10.9.1", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/optimize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/optimize/-/optimize-2.0.0.tgz", - "integrity": "sha512-nhdT+CRGDZ+bk68ic+Jw1OZ99YCDIKYA5AlVAnBHJvMawSx9YQqQAIj4refNc1/LRieGiuWvhbG3jvPVYho0Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/relay-operation-optimizer": { - "version": "7.0.21", - "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.21.tgz", - "integrity": "sha512-vMdU0+XfeBh9RCwPqRsr3A05hPA3MsahFn/7OAwXzMySA5EVnSH5R4poWNs3h1a0yT0tDPLhxORhK7qJdSWj2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ardatan/relay-compiler": "^12.0.3", - "@graphql-tools/utils": "^10.9.1", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/schema": { - "version": "10.0.25", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.25.tgz", - "integrity": "sha512-/PqE8US8kdQ7lB9M5+jlW8AyVjRGCKU7TSktuW3WNKSKmDO0MK1wakvb5gGdyT49MjAIb4a3LWxIpwo5VygZuw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/merge": "^9.1.1", - "@graphql-tools/utils": "^10.9.1", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/url-loader": { - "version": "8.0.33", - "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-8.0.33.tgz", - "integrity": "sha512-Fu626qcNHcqAj8uYd7QRarcJn5XZ863kmxsg1sm0fyjyfBJnsvC7ddFt6Hayz5kxVKfsnjxiDfPMXanvsQVBKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/executor-graphql-ws": "^2.0.1", - "@graphql-tools/executor-http": "^1.1.9", - "@graphql-tools/executor-legacy-ws": "^1.1.19", - "@graphql-tools/utils": "^10.9.1", - "@graphql-tools/wrap": "^10.0.16", - "@types/ws": "^8.0.0", - "@whatwg-node/fetch": "^0.10.0", - "@whatwg-node/promise-helpers": "^1.0.0", - "isomorphic-ws": "^5.0.0", - "sync-fetch": "0.6.0-2", - "tslib": "^2.4.0", - "ws": "^8.17.1" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/utils": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.9.1.tgz", - "integrity": "sha512-B1wwkXk9UvU7LCBkPs8513WxOQ2H8Fo5p8HR1+Id9WmYE5+bd51vqN+MbrqvWczHCH2gwkREgHJN88tE0n1FCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-typed-document-node/core": "^3.1.1", - "@whatwg-node/promise-helpers": "^1.0.0", - "cross-inspect": "1.0.1", - "dset": "^3.1.4", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/wrap": { - "version": "10.1.4", - "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-10.1.4.tgz", - "integrity": "sha512-7pyNKqXProRjlSdqOtrbnFRMQAVamCmEREilOXtZujxY6kYit3tvWWSjUrcIOheltTffoRh7EQSjpy2JDCzasg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/delegate": "^10.2.23", - "@graphql-tools/schema": "^10.0.25", - "@graphql-tools/utils": "^10.9.1", - "@whatwg-node/promise-helpers": "^1.3.0", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/wrap/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@graphql-typed-document-node/core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", - "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@inquirer/ansi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.0.tgz", - "integrity": "sha512-JWaTfCxI1eTmJ1BIv86vUfjVatOdxwD0DAVKYevY8SazeUUZtW+tNbsdejVO1GYE0GXJW1N1ahmiC3TFd+7wZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/checkbox": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.4.tgz", - "integrity": "sha512-2n9Vgf4HSciFq8ttKXk+qy+GsyTXPV1An6QAwe/8bkbbqvG4VW1I/ZY1pNu2rf+h9bdzMLPbRSfcNxkHBy/Ydw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.0", - "@inquirer/core": "^10.2.2", - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/confirm": { - "version": "5.1.18", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.18.tgz", - "integrity": "sha512-MilmWOzHa3Ks11tzvuAmFoAd/wRuaP3SwlT1IZhyMke31FKLxPiuDWcGXhU+PKveNOpAc4axzAgrgxuIJJRmLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.2.2", - "@inquirer/type": "^3.0.8" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.2.2.tgz", - "integrity": "sha512-yXq/4QUnk4sHMtmbd7irwiepjB8jXU0kkFRL4nr/aDBA2mDz13cMakEWdDwX3eSCTkk03kwcndD1zfRAIlELxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.0", - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/editor": { - "version": "4.2.20", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.20.tgz", - "integrity": "sha512-7omh5y5bK672Q+Brk4HBbnHNowOZwrb/78IFXdrEB9PfdxL3GudQyDk8O9vQ188wj3xrEebS2M9n18BjJoI83g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.2.2", - "@inquirer/external-editor": "^1.0.2", - "@inquirer/type": "^3.0.8" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/expand": { - "version": "4.0.20", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.20.tgz", - "integrity": "sha512-Dt9S+6qUg94fEvgn54F2Syf0Z3U8xmnBI9ATq2f5h9xt09fs2IJXSCIXyyVHwvggKWFXEY/7jATRo2K6Dkn6Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.2.2", - "@inquirer/type": "^3.0.8", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/external-editor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.2.tgz", - "integrity": "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^2.1.0", - "iconv-lite": "^0.7.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/figures": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", - "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/input": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.4.tgz", - "integrity": "sha512-cwSGpLBMwpwcZZsc6s1gThm0J+it/KIJ+1qFL2euLmSKUMGumJ5TcbMgxEjMjNHRGadouIYbiIgruKoDZk7klw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.2.2", - "@inquirer/type": "^3.0.8" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/number": { - "version": "3.0.20", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.20.tgz", - "integrity": "sha512-bbooay64VD1Z6uMfNehED2A2YOPHSJnQLs9/4WNiV/EK+vXczf/R988itL2XLDGTgmhMF2KkiWZo+iEZmc4jqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.2.2", - "@inquirer/type": "^3.0.8" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/password": { - "version": "4.0.20", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.20.tgz", - "integrity": "sha512-nxSaPV2cPvvoOmRygQR+h0B+Av73B01cqYLcr7NXcGXhbmsYfUb8fDdw2Us1bI2YsX+VvY7I7upgFYsyf8+Nug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.0", - "@inquirer/core": "^10.2.2", - "@inquirer/type": "^3.0.8" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/prompts": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.6.tgz", - "integrity": "sha512-68JhkiojicX9SBUD8FE/pSKbOKtwoyaVj1kwqLfvjlVXZvOy3iaSWX4dCLsZyYx/5Ur07Fq+yuDNOen+5ce6ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/checkbox": "^4.2.4", - "@inquirer/confirm": "^5.1.18", - "@inquirer/editor": "^4.2.20", - "@inquirer/expand": "^4.0.20", - "@inquirer/input": "^4.2.4", - "@inquirer/number": "^3.0.20", - "@inquirer/password": "^4.0.20", - "@inquirer/rawlist": "^4.1.8", - "@inquirer/search": "^3.1.3", - "@inquirer/select": "^4.3.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/rawlist": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.8.tgz", - "integrity": "sha512-CQ2VkIASbgI2PxdzlkeeieLRmniaUU1Aoi5ggEdm6BIyqopE9GuDXdDOj9XiwOqK5qm72oI2i6J+Gnjaa26ejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.2.2", - "@inquirer/type": "^3.0.8", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/search": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.1.3.tgz", - "integrity": "sha512-D5T6ioybJJH0IiSUK/JXcoRrrm8sXwzrVMjibuPs+AgxmogKslaafy1oxFiorNI4s3ElSkeQZbhYQgLqiL8h6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.2.2", - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/select": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.3.4.tgz", - "integrity": "sha512-Qp20nySRmfbuJBBsgPU7E/cL62Hf250vMZRzYDcBHty2zdD1kKCnoDFWRr0WO2ZzaXp3R7a4esaVGJUx0E6zvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.0", - "@inquirer/core": "^10.2.2", - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/type": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", - "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@repeaterjs/repeater": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.6.tgz", - "integrity": "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@theguild/federation-composition": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@theguild/federation-composition/-/federation-composition-0.19.1.tgz", - "integrity": "sha512-E4kllHSRYh+FsY0VR+fwl0rmWhDV8xUgWawLZTXmy15nCWQwj0BDsoEpdEXjPh7xes+75cRaeJcSbZ4jkBuSdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "constant-case": "^3.0.4", - "debug": "4.4.1", - "json5": "^2.2.3", - "lodash.sortby": "^4.7.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "graphql": "^16.0.0" - } - }, - "node_modules/@theguild/federation-composition/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.0.tgz", - "integrity": "sha512-y1dMvuvJspJiPSDZUQ+WMBvF7dpnEqN4x9DDC9ie5Fs/HUZJA3wFp7EhHoVaKX/iI0cRoECV8X2jL8zi0xrHCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.12.0" - } - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@whatwg-node/disposablestack": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz", - "integrity": "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@whatwg-node/promise-helpers": "^1.0.0", - "tslib": "^2.6.3" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@whatwg-node/fetch": { - "version": "0.10.10", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.10.tgz", - "integrity": "sha512-watz4i/Vv4HpoJ+GranJ7HH75Pf+OkPQ63NoVmru6Srgc8VezTArB00i/oQlnn0KWh14gM42F22Qcc9SU9mo/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@whatwg-node/node-fetch": "^0.7.25", - "urlpattern-polyfill": "^10.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@whatwg-node/node-fetch": { - "version": "0.7.25", - "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.25.tgz", - "integrity": "sha512-szCTESNJV+Xd56zU6ShOi/JWROxE9IwCic8o5D9z5QECZloas6Ez5tUuKqXTAdu6fHFx1t6C+5gwj8smzOLjtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@fastify/busboy": "^3.1.1", - "@whatwg-node/disposablestack": "^0.0.6", - "@whatwg-node/promise-helpers": "^1.3.2", - "tslib": "^2.6.3" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@whatwg-node/promise-helpers": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@whatwg-node/promise-helpers/-/promise-helpers-1.3.2.tgz", - "integrity": "sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.6.3" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-escapes": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.0.tgz", - "integrity": "sha512-YdhtCd19sKRKfAAUsrcC1wzm4JuzJoiX4pOJqIoW2qmKj5WzG/dL8uUJ0361zaXtHqK7gEhOwtAtz7t3Yq3X5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/auto-bind": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz", - "integrity": "sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.4.tgz", - "integrity": "sha512-L+YvJwGAgwJBV1p6ffpSTa2KRc69EeeYGYjRVWKs0GKrK+LON0GC0gV+rKSNtALEDvMDqkvCFq9r1r94/Gjwxw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.0.tgz", - "integrity": "sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.8.2", - "caniuse-lite": "^1.0.30001741", - "electron-to-chromium": "^1.5.218", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camel-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001741", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", - "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/capital-case": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", - "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case-first": "^2.0.2" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/change-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", - "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "camel-case": "^4.1.2", - "capital-case": "^1.0.4", - "constant-case": "^3.0.4", - "dot-case": "^3.0.4", - "header-case": "^2.0.4", - "no-case": "^3.0.4", - "param-case": "^3.0.4", - "pascal-case": "^3.1.2", - "path-case": "^3.0.4", - "sentence-case": "^3.0.4", - "snake-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/change-case-all": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/change-case-all/-/change-case-all-1.0.15.tgz", - "integrity": "sha512-3+GIFhk3sNuvFAJKU46o26OdzudQlPNBCu1ZQi3cMeMHhty1bhDxu2WrEilVNYaGvqUtR1VSigFcJOiS13dRhQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "change-case": "^4.1.2", - "is-lower-case": "^2.0.2", - "is-upper-case": "^2.0.2", - "lower-case": "^2.0.2", - "lower-case-first": "^2.0.2", - "sponge-case": "^1.0.1", - "swap-case": "^2.0.2", - "title-case": "^3.0.3", - "upper-case": "^2.0.2", - "upper-case-first": "^2.0.2" - } - }, - "node_modules/chardet": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", - "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.0.tgz", - "integrity": "sha512-7JDGG+4Zp0CsknDCedl0DYdaeOhc46QNpXi3NLQblkZpXXgA6LncLDUUyvrjSvZeF3VRQa+KiMGomazQrC1V8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^7.1.0", - "string-width": "^8.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/common-tags": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/constant-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", - "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case": "^2.0.2" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-fetch": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", - "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, - "node_modules/cross-inspect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cross-inspect/-/cross-inspect-1.0.1.tgz", - "integrity": "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/dataloader": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", - "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==", - "dev": true, - "license": "MIT" - }, - "node_modules/debounce": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz", - "integrity": "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dependency-graph": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", - "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/dset": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", - "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.218", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz", - "integrity": "sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fbjs": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz", - "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-fetch": "^3.1.5", - "fbjs-css-vars": "^1.0.0", - "loose-envify": "^1.0.0", - "object-assign": "^4.1.0", - "promise": "^7.1.1", - "setimmediate": "^1.0.5", - "ua-parser-js": "^1.0.35" - } - }, - "node_modules/fbjs-css-vars": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", - "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graphql": { - "version": "16.11.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", - "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/graphql-config": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-5.1.5.tgz", - "integrity": "sha512-mG2LL1HccpU8qg5ajLROgdsBzx/o2M6kgI3uAmoaXiSH9PCUbtIyLomLqUtCFaAeG2YCFsl0M5cfQ9rKmDoMVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/graphql-file-loader": "^8.0.0", - "@graphql-tools/json-file-loader": "^8.0.0", - "@graphql-tools/load": "^8.1.0", - "@graphql-tools/merge": "^9.0.0", - "@graphql-tools/url-loader": "^8.0.0", - "@graphql-tools/utils": "^10.0.0", - "cosmiconfig": "^8.1.0", - "jiti": "^2.0.0", - "minimatch": "^9.0.5", - "string-env-interpolation": "^1.0.1", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">= 16.0.0" - }, - "peerDependencies": { - "cosmiconfig-toml-loader": "^1.0.0", - "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - }, - "peerDependenciesMeta": { - "cosmiconfig-toml-loader": { - "optional": true - } - } - }, - "node_modules/graphql-config/node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/graphql-tag": { - "version": "2.12.6", - "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", - "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/graphql-ws": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.6.tgz", - "integrity": "sha512-zgfER9s+ftkGKUZgc0xbx8T7/HMO4AV5/YuYiFc+AtgcO5T0v8AxYYNQ+ltzuzDZgNkYJaFspm5MMYLjQzrkmw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@fastify/websocket": "^10 || ^11", - "crossws": "~0.3", - "graphql": "^15.10.1 || ^16", - "uWebSockets.js": "^20", - "ws": "^8" - }, - "peerDependenciesMeta": { - "@fastify/websocket": { - "optional": true - }, - "crossws": { - "optional": true - }, - "uWebSockets.js": { - "optional": true - }, - "ws": { - "optional": true - } - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/header-case": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", - "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "capital-case": "^1.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immutable": { - "version": "3.7.6", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz", - "integrity": "sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/import-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-from/-/import-from-4.0.0.tgz", - "integrity": "sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, - "node_modules/is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-lower-case/-/is-lower-case-2.0.2.tgz", - "integrity": "sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-unc-path": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "unc-path-regex": "^0.1.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-upper-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-2.0.2.tgz", - "integrity": "sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isomorphic-ws": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", - "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/jiti": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", - "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-to-pretty-yaml": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/json-to-pretty-yaml/-/json-to-pretty-yaml-1.2.2.tgz", - "integrity": "sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "remedial": "^1.0.7", - "remove-trailing-spaces": "^1.0.6" - }, - "engines": { - "node": ">= 0.2.0" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.4.tgz", - "integrity": "sha512-1wd/kpAdKRLwv7/3OKC8zZ5U8e/fajCfWMxacUvB79S5nLrYGPtUI/8chMQhn3LQjsRVErTb9i1ECAwW0ZIHnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^5.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/listr2/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/listr2/node_modules/emoji-regex": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", - "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", - "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/lower-case-first": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case-first/-/lower-case-first-2.0.2.tgz", - "integrity": "sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, - "node_modules/map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/meros": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/meros/-/meros-1.3.2.tgz", - "integrity": "sha512-Q3mobPbvEx7XbwhnC1J1r60+5H6EZyNccdzSz0eGexJRwouUtTZxPVRGdqKtxlpD84ScK4+tIGldkqDtCKdI0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=13" - }, - "peerDependencies": { - "@types/node": ">=13" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", - "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nullthrows": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", - "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/param-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "dev": true, - "license": "MIT", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-filepath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/path-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", - "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-root-regex": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "asap": "~2.0.3" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/relay-runtime": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-12.0.0.tgz", - "integrity": "sha512-QU6JKr1tMsry22DXNy9Whsq5rmvwr3LSZiiWV/9+DFpuTWvp+WFhobWMc8TC4OjKFfNhEZy7mOiqUAn5atQtug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.0.0", - "fbjs": "^3.0.0", - "invariant": "^2.2.4" - } - }, - "node_modules/remedial": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/remedial/-/remedial-1.0.8.tgz", - "integrity": "sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==", - "dev": true, - "license": "(MIT OR Apache-2.0)", - "engines": { - "node": "*" - } - }, - "node_modules/remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", - "dev": true, - "license": "ISC" - }, - "node_modules/remove-trailing-spaces": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/remove-trailing-spaces/-/remove-trailing-spaces-1.0.9.tgz", - "integrity": "sha512-xzG7w5IRijvIkHIjDk65URsJJ7k4J95wmcArY5PRcmjldIOl7oTvG8+X2Ag690R7SfwiOcHrWZKVc1Pp5WIOzA==", - "dev": true, - "license": "MIT" - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/sentence-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", - "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case-first": "^2.0.2" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true, - "license": "MIT" - }, - "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/signedsource": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/signedsource/-/signedsource-1.0.0.tgz", - "integrity": "sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/snake-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", - "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "dev": true, - "license": "MIT", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/sponge-case": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sponge-case/-/sponge-case-1.0.1.tgz", - "integrity": "sha512-dblb9Et4DAtiZ5YSUZHLl4XhH4uK80GhAZrVXdN4O2P4gQ40Wa5UIOPUHlA/nFd2PLblBZWUioLMMAVrgpoYcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/string-env-interpolation": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string-env-interpolation/-/string-env-interpolation-1.0.1.tgz", - "integrity": "sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/swap-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/swap-case/-/swap-case-2.0.2.tgz", - "integrity": "sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/sync-fetch": { - "version": "0.6.0-2", - "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.6.0-2.tgz", - "integrity": "sha512-c7AfkZ9udatCuAy9RSfiGPpeOKKUAUK5e1cXadLOGUjasdxqYqAK0jTNkM/FSEyJ3a5Ra27j/tw/PS0qLmaF/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "node-fetch": "^3.3.2", - "timeout-signal": "^2.0.0", - "whatwg-mimetype": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/sync-fetch/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/timeout-signal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/timeout-signal/-/timeout-signal-2.0.0.tgz", - "integrity": "sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/title-case": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", - "integrity": "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ts-log": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/ts-log/-/ts-log-2.2.7.tgz", - "integrity": "sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg==", - "dev": true, - "license": "MIT" - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true, - "license": "0BSD" - }, - "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ua-parser-js": { - "version": "1.0.41", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", - "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "license": "MIT", - "bin": { - "ua-parser-js": "script/cli.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/undici-types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unixify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz", - "integrity": "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "normalize-path": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/upper-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", - "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/upper-case-first": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", - "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/urlpattern-polyfill": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", - "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", - "dev": true, - "license": "MIT" - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yoctocolors-cjs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", - "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/packages/gql/package.json b/packages/gql/package.json index 402a5048..d1f09623 100644 --- a/packages/gql/package.json +++ b/packages/gql/package.json @@ -1,10 +1,11 @@ { "name": "@hyodotdev/openiap-gql", - "version": "2.0.0", + "version": "2.0.2", "type": "module", "main": "src/generated/types.ts", "exports": { ".": "./src/generated/types.ts", + "./kit-api": "./src/kit-api.ts", "./webhook-client": "./src/webhook-client.ts", "./swift": "./src/generated/Types.swift", "./kotlin": "./src/generated/Types.kt", @@ -40,7 +41,7 @@ "handlebars": "^4.7.8", "ts-node": "^10.9.2", "typescript": "^5.9.2", - "vitest": "^4" + "vitest": "^4.1.5" }, - "packageManager": "bun@1.1.0" + "packageManager": "bun@1.3.13" } diff --git a/packages/gql/src/api-android.graphql b/packages/gql/src/api-android.graphql index 2530e2d2..a83a04d9 100644 --- a/packages/gql/src/api-android.graphql +++ b/packages/gql/src/api-android.graphql @@ -3,13 +3,13 @@ extend type Mutation { """ Acknowledge a non-consumable purchase. Required within 3 days or Google auto-refunds. - See: https://www.openiap.dev/docs/apis/android/acknowledge-purchase-android + See: https://openiap.dev/docs/apis/android/acknowledge-purchase-android """ # Future acknowledgePurchaseAndroid(purchaseToken: String!): Boolean! """ Consume a consumable purchase so it can be re-bought. - See: https://www.openiap.dev/docs/apis/android/consume-purchase-android + See: https://openiap.dev/docs/apis/android/consume-purchase-android """ # Future consumePurchaseAndroid(purchaseToken: String!): Boolean! @@ -20,7 +20,7 @@ extend type Mutation { Returns true if available, false otherwise. Throws OpenIapError.NotPrepared if billing client not ready. - See: https://www.openiap.dev/docs/apis/android/check-alternative-billing-availability-android + See: https://openiap.dev/docs/apis/android/check-alternative-billing-availability-android """ # Future checkAlternativeBillingAvailabilityAndroid: Boolean! @@ -30,7 +30,7 @@ extend type Mutation { Returns true if user accepted, false if user canceled. Throws OpenIapError.NotPrepared if billing client not ready. - See: https://www.openiap.dev/docs/apis/android/show-alternative-billing-dialog-android + See: https://openiap.dev/docs/apis/android/show-alternative-billing-dialog-android """ # Future showAlternativeBillingDialogAndroid: Boolean! @@ -41,7 +41,7 @@ extend type Mutation { Returns token string, or null if creation failed. Throws OpenIapError.NotPrepared if billing client not ready. - See: https://www.openiap.dev/docs/apis/android/create-alternative-billing-token-android + See: https://openiap.dev/docs/apis/android/create-alternative-billing-token-android """ # Future createAlternativeBillingTokenAndroid: String @@ -54,7 +54,7 @@ extend type Mutation { Available in Google Play Billing Library 8.2.0+. Returns availability result with isAvailable flag. Throws OpenIapError.NotPrepared if billing client not ready. - See: https://www.openiap.dev/docs/apis/android/is-billing-program-available-android + See: https://openiap.dev/docs/apis/android/is-billing-program-available-android """ # Future isBillingProgramAvailableAndroid(program: BillingProgramAndroid!): BillingProgramAvailabilityResultAndroid! @@ -65,7 +65,7 @@ extend type Mutation { Returns external transaction token needed for reporting external transactions. Throws OpenIapError.NotPrepared if billing client not ready. - See: https://www.openiap.dev/docs/apis/android/create-billing-program-reporting-details-android + See: https://openiap.dev/docs/apis/android/create-billing-program-reporting-details-android """ # Future createBillingProgramReportingDetailsAndroid(program: BillingProgramAndroid!): BillingProgramReportingDetailsAndroid! @@ -76,7 +76,7 @@ extend type Mutation { Shows Play Store dialog and optionally launches external URL. Throws OpenIapError.NotPrepared if billing client not ready. - See: https://www.openiap.dev/docs/apis/android/launch-external-link-android + See: https://openiap.dev/docs/apis/android/launch-external-link-android """ # Future launchExternalLinkAndroid(params: LaunchExternalLinkParamsAndroid!): Boolean! diff --git a/packages/gql/src/api-ios.graphql b/packages/gql/src/api-ios.graphql index a9494f2e..5492c9fb 100644 --- a/packages/gql/src/api-ios.graphql +++ b/packages/gql/src/api-ios.graphql @@ -3,20 +3,20 @@ extend type Query { """ Deprecated. Get the current App Store storefront country code — use cross-platform getStorefront instead. - See: https://www.openiap.dev/docs/apis/ios/get-storefront-ios + See: https://openiap.dev/docs/apis/ios/get-storefront-ios """ # Future getStorefrontIOS: String! @deprecated(reason: "Use getStorefront") """ Read the App Store-promoted product, if any (iOS 11+). - See: https://www.openiap.dev/docs/apis/ios/get-promoted-product-ios + See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios """ # Future getPromotedProductIOS: ProductIOS """ Check eligibility for the external purchase notice sheet (iOS 17.4+). Uses ExternalPurchase.canPresent. - See: https://www.openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios + See: https://openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios """ # Future canPresentExternalPurchaseNoticeIOS: Boolean! @@ -24,7 +24,7 @@ extend type Query { Check eligibility for the custom-link variant of external purchase (iOS 18.1+). Returns true if the app can use custom external purchase links. Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/iseligible - See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios + See: https://openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios """ # Future isEligibleForExternalPurchaseCustomLinkIOS: Boolean! @@ -32,7 +32,7 @@ extend type Query { Fetch a token for Apple's External Purchase Server reporting API (iOS 18.1+). Use this token to report transactions made through ExternalPurchaseCustomLink. Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) - See: https://www.openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios + See: https://openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios """ # Future getExternalPurchaseCustomLinkTokenIOS( @@ -43,55 +43,55 @@ extend type Query { ): ExternalPurchaseCustomLinkTokenResultIOS! """ List unfinished StoreKit transactions in the queue. - See: https://www.openiap.dev/docs/apis/ios/get-pending-transactions-ios + See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios """ # Future getPendingTransactionsIOS: [PurchaseIOS!]! """ Check intro-offer eligibility for a subscription group. - See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios + See: https://openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios """ # Future isEligibleForIntroOfferIOS(groupID: String!): Boolean! """ Get subscription status objects from StoreKit 2 (iOS 15+). - See: https://www.openiap.dev/docs/apis/ios/subscription-status-ios + See: https://openiap.dev/docs/apis/ios/subscription-status-ios """ # Future subscriptionStatusIOS(sku: String!): [SubscriptionStatusIOS!]! """ Get the user's current entitlement for a product, using StoreKit 2 (iOS 15+). - See: https://www.openiap.dev/docs/apis/ios/current-entitlement-ios + See: https://openiap.dev/docs/apis/ios/current-entitlement-ios """ # Future currentEntitlementIOS(sku: String!): PurchaseIOS """ Get the latest verified transaction for a product, using StoreKit 2. - See: https://www.openiap.dev/docs/apis/ios/latest-transaction-ios + See: https://openiap.dev/docs/apis/ios/latest-transaction-ios """ # Future latestTransactionIOS(sku: String!): PurchaseIOS """ Check whether a transaction's JWS verification passed (StoreKit 2). - See: https://www.openiap.dev/docs/apis/ios/is-transaction-verified-ios + See: https://openiap.dev/docs/apis/ios/is-transaction-verified-ios """ # Future isTransactionVerifiedIOS(sku: String!): Boolean! """ Return the JWS string for a transaction (StoreKit 2). - See: https://www.openiap.dev/docs/apis/ios/get-transaction-jws-ios + See: https://openiap.dev/docs/apis/ios/get-transaction-jws-ios """ # Future getTransactionJwsIOS(sku: String!): String """ Get base64-encoded receipt data (legacy validation). - See: https://www.openiap.dev/docs/apis/ios/get-receipt-data-ios + See: https://openiap.dev/docs/apis/ios/get-receipt-data-ios """ # Future getReceiptDataIOS: String """ Fetch the app transaction (iOS 16+). - See: https://www.openiap.dev/docs/apis/ios/get-app-transaction-ios + See: https://openiap.dev/docs/apis/ios/get-app-transaction-ios """ # Future getAppTransactionIOS: AppTransaction @@ -100,13 +100,13 @@ extend type Query { Requires the SK2ConsumableTransactionHistory Info.plist key in the host app for finished consumables to be included (iOS 18+). Unlike getAvailablePurchases, always returns the iOS-specific PurchaseIOS shape. - See: https://www.openiap.dev/docs/apis/ios/get-all-transactions-ios + See: https://openiap.dev/docs/apis/ios/get-all-transactions-ios """ # Future getAllTransactionsIOS: [PurchaseIOS!]! """ Deprecated. Legacy App Store receipt validation — use verifyPurchase instead. - See: https://www.openiap.dev/docs/apis/ios/validate-receipt-ios + See: https://openiap.dev/docs/apis/ios/validate-receipt-ios """ # Future validateReceiptIOS(options: VerifyPurchaseProps!): VerifyPurchaseResultIOS! @deprecated(reason: "Use verifyPurchase") @@ -115,7 +115,7 @@ extend type Query { extend type Mutation { """ Clear pending transactions in the queue (sandbox helper). - See: https://www.openiap.dev/docs/apis/ios/clear-transaction-ios + See: https://openiap.dev/docs/apis/ios/clear-transaction-ios """ # Future clearTransactionIOS: Boolean! @@ -125,31 +125,31 @@ extend type Mutation { @deprecated Use promotedProductListenerIOS to receive the productId, then call requestPurchase with that SKU instead. In StoreKit 2, promoted products can be purchased directly via the standard purchase flow. - See: https://www.openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios + See: https://openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios """ # Future requestPurchaseOnPromotedProductIOS: Boolean! @deprecated(reason: "Use promotedProductListenerIOS + requestPurchase instead") """ Present the manage-subscriptions sheet and return changed purchases (iOS 15+). - See: https://www.openiap.dev/docs/apis/ios/show-manage-subscriptions-ios + See: https://openiap.dev/docs/apis/ios/show-manage-subscriptions-ios """ # Future showManageSubscriptionsIOS: [PurchaseIOS!]! """ Present the refund request sheet (iOS 15+). See also Features → Refund. - See: https://www.openiap.dev/docs/apis/ios/begin-refund-request-ios + See: https://openiap.dev/docs/apis/ios/begin-refund-request-ios """ # Future beginRefundRequestIOS(sku: String!): String """ Force sync transactions with the App Store (iOS 15+). - See: https://www.openiap.dev/docs/apis/ios/sync-ios + See: https://openiap.dev/docs/apis/ios/sync-ios """ # Future syncIOS: Boolean! """ Show the App Store offer code redemption sheet. - See: https://www.openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios """ # Future presentCodeRedemptionSheetIOS: Boolean! @@ -157,13 +157,13 @@ extend type Mutation { Present the external purchase notice sheet (iOS 17.4+). Uses ExternalPurchase.presentNoticeSheet() which returns a token when the user continues. Reference: https://developer.apple.com/documentation/storekit/externalpurchase/presentnoticesheet() - See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios + See: https://openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios """ # Future presentExternalPurchaseNoticeSheetIOS: ExternalPurchaseNoticeResultIOS! """ Present an external purchase link, StoreKit External (iOS 16+). - See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-link-ios + See: https://openiap.dev/docs/apis/ios/present-external-purchase-link-ios """ # Future presentExternalPurchaseLinkIOS(url: String!): ExternalPurchaseLinkResultIOS! @@ -171,7 +171,7 @@ extend type Mutation { Present the disclosure sheet required before linking out via ExternalPurchaseCustomLink (iOS 18.1+). Call this after a deliberate customer interaction before linking out to external purchases. Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/shownotice(type:) - See: https://www.openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios + See: https://openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios """ # Future showExternalPurchaseCustomLinkNoticeIOS( diff --git a/packages/gql/src/api.graphql b/packages/gql/src/api.graphql index 8c7e357b..e7f1f6a4 100644 --- a/packages/gql/src/api.graphql +++ b/packages/gql/src/api.graphql @@ -4,31 +4,31 @@ extend type Query { """ Fetch products or subscriptions from the store. - See: https://www.openiap.dev/docs/apis/fetch-products + See: https://openiap.dev/docs/apis/fetch-products """ # Future fetchProducts(params: ProductRequest!): FetchProductsResult! """ List active purchases for the current user. - See: https://www.openiap.dev/docs/apis/get-available-purchases + See: https://openiap.dev/docs/apis/get-available-purchases """ # Future getAvailablePurchases(options: PurchaseOptions): [Purchase!]! """ Get details of all currently active subscriptions (filters by subscriptionIds when provided). - See: https://www.openiap.dev/docs/apis/get-active-subscriptions + See: https://openiap.dev/docs/apis/get-active-subscriptions """ # Future getActiveSubscriptions(subscriptionIds: [String!]): [ActiveSubscription!]! """ Check whether the user has any active subscription. - See: https://www.openiap.dev/docs/apis/has-active-subscriptions + See: https://openiap.dev/docs/apis/has-active-subscriptions """ # Future hasActiveSubscriptions(subscriptionIds: [String!]): Boolean! """ Return the user's storefront country code. - See: https://www.openiap.dev/docs/apis/get-storefront + See: https://openiap.dev/docs/apis/get-storefront """ # Future getStorefront: String! @@ -38,25 +38,25 @@ extend type Query { extend type Mutation { """ Initialize the store connection. Call before any IAP API. - See: https://www.openiap.dev/docs/apis/init-connection + See: https://openiap.dev/docs/apis/init-connection """ # Future initConnection(config: InitConnectionConfig): Boolean! """ Close the store connection and release resources. - See: https://www.openiap.dev/docs/apis/end-connection + See: https://openiap.dev/docs/apis/end-connection """ # Future endConnection: Boolean! """ Initiate a purchase or subscription flow; rely on events for final state. - See: https://www.openiap.dev/docs/apis/request-purchase + See: https://openiap.dev/docs/apis/request-purchase """ # Future requestPurchase(params: RequestPurchaseProps!): RequestPurchaseResult """ Complete a transaction after server-side verification. Required on Android within 3 days. - See: https://www.openiap.dev/docs/apis/finish-transaction + See: https://openiap.dev/docs/apis/finish-transaction """ # Future finishTransaction( @@ -65,19 +65,19 @@ extend type Mutation { ): VoidResult! """ Restore non-consumable and active subscription purchases. - See: https://www.openiap.dev/docs/apis/restore-purchases + See: https://openiap.dev/docs/apis/restore-purchases """ # Future restorePurchases: VoidResult! """ Open the platform's subscription management UI. - See: https://www.openiap.dev/docs/apis/deep-link-to-subscriptions + See: https://openiap.dev/docs/apis/deep-link-to-subscriptions """ # Future deepLinkToSubscriptions(options: DeepLinkOptions): VoidResult! """ Deprecated. Validate purchase receipts with the configured providers — use verifyPurchase instead. - See: https://www.openiap.dev/docs/features/validation#verify-purchase + See: https://openiap.dev/docs/features/validation#verify-purchase """ # Future validateReceipt(options: VerifyPurchaseProps!): VerifyPurchaseResult! @deprecated(reason: "Use verifyPurchase") @@ -87,7 +87,7 @@ extend type Mutation { + receipt/JWS metadata, VerifyPurchaseResultAndroid carries Play Store receipt fields (no isValid), and VerifyPurchaseResultHorizon uses success. Inspect the concrete variant before reading fields. - See: https://www.openiap.dev/docs/features/validation#verify-purchase + See: https://openiap.dev/docs/features/validation#verify-purchase """ # Future verifyPurchase(options: VerifyPurchaseProps!): VerifyPurchaseResult! @@ -95,7 +95,7 @@ extend type Mutation { Verify via a managed provider without standing up your own server. The PurchaseVerificationProvider enum currently exposes only IAPKit; platform availability may differ by implementation. - See: https://www.openiap.dev/docs/features/validation#verify-purchase-with-provider + See: https://openiap.dev/docs/features/validation#verify-purchase-with-provider """ # Future verifyPurchaseWithProvider( diff --git a/packages/gql/src/generated/Types.cs b/packages/gql/src/generated/Types.cs index e89111dd..6c5ea212 100644 --- a/packages/gql/src/generated/Types.cs +++ b/packages/gql/src/generated/Types.cs @@ -4100,26 +4100,26 @@ public sealed record WinBackOfferInputIOS public interface MutationResolver { /// Acknowledge a non-consumable purchase. Required within 3 days or Google auto-refunds. - /// See: https://www.openiap.dev/docs/apis/android/acknowledge-purchase-android + /// See: https://openiap.dev/docs/apis/android/acknowledge-purchase-android Task AcknowledgePurchaseAndroidAsync(string purchaseToken); /// Present the refund request sheet (iOS 15+). See also Features → Refund. - /// See: https://www.openiap.dev/docs/apis/ios/begin-refund-request-ios + /// See: https://openiap.dev/docs/apis/ios/begin-refund-request-ios Task BeginRefundRequestIOSAsync(string sku); /// Check whether alternative billing is available for the user. Step 1 of the alternative billing flow. /// /// Returns true if available, false otherwise. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/check-alternative-billing-availability-android + /// See: https://openiap.dev/docs/apis/android/check-alternative-billing-availability-android Task CheckAlternativeBillingAvailabilityAndroidAsync(); /// Clear pending transactions in the queue (sandbox helper). - /// See: https://www.openiap.dev/docs/apis/ios/clear-transaction-ios + /// See: https://openiap.dev/docs/apis/ios/clear-transaction-ios Task ClearTransactionIOSAsync(); /// Consume a consumable purchase so it can be re-bought. - /// See: https://www.openiap.dev/docs/apis/android/consume-purchase-android + /// See: https://openiap.dev/docs/apis/android/consume-purchase-android Task ConsumePurchaseAndroidAsync(string purchaseToken); /// Create a reporting token for an alternative billing flow. Step 3 of the alternative billing flow. @@ -4128,7 +4128,7 @@ public interface MutationResolver /// /// Returns token string, or null if creation failed. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/create-alternative-billing-token-android + /// See: https://openiap.dev/docs/apis/android/create-alternative-billing-token-android Task CreateAlternativeBillingTokenAndroidAsync(); /// Create the reporting payload Google requires after a Developer-Provided Billing transaction (Play Billing 8.3.0+). @@ -4136,23 +4136,23 @@ public interface MutationResolver /// /// Returns external transaction token needed for reporting external transactions. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/create-billing-program-reporting-details-android + /// See: https://openiap.dev/docs/apis/android/create-billing-program-reporting-details-android Task CreateBillingProgramReportingDetailsAndroidAsync(BillingProgramAndroid program); /// Open the platform's subscription management UI. - /// See: https://www.openiap.dev/docs/apis/deep-link-to-subscriptions + /// See: https://openiap.dev/docs/apis/deep-link-to-subscriptions Task DeepLinkToSubscriptionsAsync(DeepLinkOptions? options = null); /// Close the store connection and release resources. - /// See: https://www.openiap.dev/docs/apis/end-connection + /// See: https://openiap.dev/docs/apis/end-connection Task EndConnectionAsync(); /// Complete a transaction after server-side verification. Required on Android within 3 days. - /// See: https://www.openiap.dev/docs/apis/finish-transaction + /// See: https://openiap.dev/docs/apis/finish-transaction Task FinishTransactionAsync(PurchaseInput purchase, bool? isConsumable = null); /// Initialize the store connection. Call before any IAP API. - /// See: https://www.openiap.dev/docs/apis/init-connection + /// See: https://openiap.dev/docs/apis/init-connection Task InitConnectionAsync(InitConnectionConfig? config = null); /// Check whether a billing program (e.g., External Payments) is available for the current user. @@ -4161,7 +4161,7 @@ public interface MutationResolver /// Available in Google Play Billing Library 8.2.0+. /// Returns availability result with isAvailable flag. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/is-billing-program-available-android + /// See: https://openiap.dev/docs/apis/android/is-billing-program-available-android Task IsBillingProgramAvailableAndroidAsync(BillingProgramAndroid program); /// Launch an external content/offer link from inside the Billing Programs flow (Play Billing 8.2.0+). @@ -4169,25 +4169,25 @@ public interface MutationResolver /// /// Shows Play Store dialog and optionally launches external URL. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/launch-external-link-android + /// See: https://openiap.dev/docs/apis/android/launch-external-link-android Task LaunchExternalLinkAndroidAsync(LaunchExternalLinkParamsAndroid @params); /// Show the App Store offer code redemption sheet. - /// See: https://www.openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios Task PresentCodeRedemptionSheetIOSAsync(); /// Present an external purchase link, StoreKit External (iOS 16+). - /// See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-link-ios + /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-link-ios Task PresentExternalPurchaseLinkIOSAsync(string url); /// Present the external purchase notice sheet (iOS 17.4+). /// Uses ExternalPurchase.presentNoticeSheet() which returns a token when the user continues. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchase/presentnoticesheet() - /// See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios + /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios Task PresentExternalPurchaseNoticeSheetIOSAsync(); /// Initiate a purchase or subscription flow; rely on events for final state. - /// See: https://www.openiap.dev/docs/apis/request-purchase + /// See: https://openiap.dev/docs/apis/request-purchase Task RequestPurchaseAsync(RequestPurchaseProps @params); /// Buy the currently promoted product. @@ -4195,11 +4195,11 @@ public interface MutationResolver /// @deprecated Use promotedProductListenerIOS to receive the productId, /// then call requestPurchase with that SKU instead. In StoreKit 2, /// promoted products can be purchased directly via the standard purchase flow. - /// See: https://www.openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios + /// See: https://openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios Task RequestPurchaseOnPromotedProductIOSAsync(); /// Restore non-consumable and active subscription purchases. - /// See: https://www.openiap.dev/docs/apis/restore-purchases + /// See: https://openiap.dev/docs/apis/restore-purchases Task RestorePurchasesAsync(); /// Display Google's alternative billing information dialog. Step 2 of the alternative billing flow. @@ -4207,25 +4207,25 @@ public interface MutationResolver /// /// Returns true if user accepted, false if user canceled. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/show-alternative-billing-dialog-android + /// See: https://openiap.dev/docs/apis/android/show-alternative-billing-dialog-android Task ShowAlternativeBillingDialogAndroidAsync(); /// Present the disclosure sheet required before linking out via ExternalPurchaseCustomLink (iOS 18.1+). /// Call this after a deliberate customer interaction before linking out to external purchases. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/shownotice(type:) - /// See: https://www.openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios + /// See: https://openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios Task ShowExternalPurchaseCustomLinkNoticeIOSAsync(ExternalPurchaseCustomLinkNoticeTypeIOS noticeType); /// Present the manage-subscriptions sheet and return changed purchases (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/show-manage-subscriptions-ios + /// See: https://openiap.dev/docs/apis/ios/show-manage-subscriptions-ios Task> ShowManageSubscriptionsIOSAsync(); /// Force sync transactions with the App Store (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/sync-ios + /// See: https://openiap.dev/docs/apis/ios/sync-ios Task SyncIOSAsync(); /// Deprecated. Validate purchase receipts with the configured providers — use verifyPurchase instead. - /// See: https://www.openiap.dev/docs/features/validation#verify-purchase + /// See: https://openiap.dev/docs/features/validation#verify-purchase Task ValidateReceiptAsync(VerifyPurchaseProps options); /// Verify a purchase against your own backend. Returns a platform-specific @@ -4233,13 +4233,13 @@ public interface MutationResolver /// + receipt/JWS metadata, VerifyPurchaseResultAndroid carries Play Store /// receipt fields (no isValid), and VerifyPurchaseResultHorizon uses success. /// Inspect the concrete variant before reading fields. - /// See: https://www.openiap.dev/docs/features/validation#verify-purchase + /// See: https://openiap.dev/docs/features/validation#verify-purchase Task VerifyPurchaseAsync(VerifyPurchaseProps options); /// Verify via a managed provider without standing up your own server. The /// PurchaseVerificationProvider enum currently exposes only IAPKit; platform /// availability may differ by implementation. - /// See: https://www.openiap.dev/docs/features/validation#verify-purchase-with-provider + /// See: https://openiap.dev/docs/features/validation#verify-purchase-with-provider Task VerifyPurchaseWithProviderAsync(VerifyPurchaseWithProviderProps options); } @@ -4248,94 +4248,94 @@ public interface QueryResolver { /// Check eligibility for the external purchase notice sheet (iOS 17.4+). /// Uses ExternalPurchase.canPresent. - /// See: https://www.openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios + /// See: https://openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios Task CanPresentExternalPurchaseNoticeIOSAsync(); /// Get the user's current entitlement for a product, using StoreKit 2 (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/current-entitlement-ios + /// See: https://openiap.dev/docs/apis/ios/current-entitlement-ios Task CurrentEntitlementIOSAsync(string sku); /// Fetch products or subscriptions from the store. - /// See: https://www.openiap.dev/docs/apis/fetch-products + /// See: https://openiap.dev/docs/apis/fetch-products Task FetchProductsAsync(ProductRequest @params); /// Get details of all currently active subscriptions (filters by subscriptionIds when provided). - /// See: https://www.openiap.dev/docs/apis/get-active-subscriptions + /// See: https://openiap.dev/docs/apis/get-active-subscriptions Task> GetActiveSubscriptionsAsync(IReadOnlyList? subscriptionIds = null); /// List every StoreKit transaction (finished + unfinished) for the current user. /// Requires the SK2ConsumableTransactionHistory Info.plist key in the host app /// for finished consumables to be included (iOS 18+). /// Unlike getAvailablePurchases, always returns the iOS-specific PurchaseIOS shape. - /// See: https://www.openiap.dev/docs/apis/ios/get-all-transactions-ios + /// See: https://openiap.dev/docs/apis/ios/get-all-transactions-ios Task> GetAllTransactionsIOSAsync(); /// Fetch the app transaction (iOS 16+). - /// See: https://www.openiap.dev/docs/apis/ios/get-app-transaction-ios + /// See: https://openiap.dev/docs/apis/ios/get-app-transaction-ios Task GetAppTransactionIOSAsync(); /// List active purchases for the current user. - /// See: https://www.openiap.dev/docs/apis/get-available-purchases + /// See: https://openiap.dev/docs/apis/get-available-purchases Task> GetAvailablePurchasesAsync(PurchaseOptions? options = null); /// Fetch a token for Apple's External Purchase Server reporting API (iOS 18.1+). /// Use this token to report transactions made through ExternalPurchaseCustomLink. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) - /// See: https://www.openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios + /// See: https://openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios Task GetExternalPurchaseCustomLinkTokenIOSAsync(ExternalPurchaseCustomLinkTokenTypeIOS tokenType); /// List unfinished StoreKit transactions in the queue. - /// See: https://www.openiap.dev/docs/apis/ios/get-pending-transactions-ios + /// See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios Task> GetPendingTransactionsIOSAsync(); /// Read the App Store-promoted product, if any (iOS 11+). - /// See: https://www.openiap.dev/docs/apis/ios/get-promoted-product-ios + /// See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios Task GetPromotedProductIOSAsync(); /// Get base64-encoded receipt data (legacy validation). - /// See: https://www.openiap.dev/docs/apis/ios/get-receipt-data-ios + /// See: https://openiap.dev/docs/apis/ios/get-receipt-data-ios Task GetReceiptDataIOSAsync(); /// Return the user's storefront country code. - /// See: https://www.openiap.dev/docs/apis/get-storefront + /// See: https://openiap.dev/docs/apis/get-storefront Task GetStorefrontAsync(); /// Deprecated. Get the current App Store storefront country code — use cross-platform getStorefront instead. - /// See: https://www.openiap.dev/docs/apis/ios/get-storefront-ios + /// See: https://openiap.dev/docs/apis/ios/get-storefront-ios Task GetStorefrontIOSAsync(); /// Return the JWS string for a transaction (StoreKit 2). - /// See: https://www.openiap.dev/docs/apis/ios/get-transaction-jws-ios + /// See: https://openiap.dev/docs/apis/ios/get-transaction-jws-ios Task GetTransactionJwsIOSAsync(string sku); /// Check whether the user has any active subscription. - /// See: https://www.openiap.dev/docs/apis/has-active-subscriptions + /// See: https://openiap.dev/docs/apis/has-active-subscriptions Task HasActiveSubscriptionsAsync(IReadOnlyList? subscriptionIds = null); /// Check eligibility for the custom-link variant of external purchase (iOS 18.1+). /// Returns true if the app can use custom external purchase links. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/iseligible - /// See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios + /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios Task IsEligibleForExternalPurchaseCustomLinkIOSAsync(); /// Check intro-offer eligibility for a subscription group. - /// See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios + /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios Task IsEligibleForIntroOfferIOSAsync(string groupId); /// Check whether a transaction's JWS verification passed (StoreKit 2). - /// See: https://www.openiap.dev/docs/apis/ios/is-transaction-verified-ios + /// See: https://openiap.dev/docs/apis/ios/is-transaction-verified-ios Task IsTransactionVerifiedIOSAsync(string sku); /// Get the latest verified transaction for a product, using StoreKit 2. - /// See: https://www.openiap.dev/docs/apis/ios/latest-transaction-ios + /// See: https://openiap.dev/docs/apis/ios/latest-transaction-ios Task LatestTransactionIOSAsync(string sku); /// Get subscription status objects from StoreKit 2 (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/subscription-status-ios + /// See: https://openiap.dev/docs/apis/ios/subscription-status-ios Task> SubscriptionStatusIOSAsync(string sku); /// Deprecated. Legacy App Store receipt validation — use verifyPurchase instead. - /// See: https://www.openiap.dev/docs/apis/ios/validate-receipt-ios + /// See: https://openiap.dev/docs/apis/ios/validate-receipt-ios Task ValidateReceiptIOSAsync(VerifyPurchaseProps options); } diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index 7e1e6572..7455c2c6 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -5271,12 +5271,12 @@ public sealed interface VerifyPurchaseResult { public interface MutationResolver { /** * Acknowledge a non-consumable purchase. Required within 3 days or Google auto-refunds. - * See: https://www.openiap.dev/docs/apis/android/acknowledge-purchase-android + * See: https://openiap.dev/docs/apis/android/acknowledge-purchase-android */ suspend fun acknowledgePurchaseAndroid(purchaseToken: String): Boolean /** * Present the refund request sheet (iOS 15+). See also Features → Refund. - * See: https://www.openiap.dev/docs/apis/ios/begin-refund-request-ios + * See: https://openiap.dev/docs/apis/ios/begin-refund-request-ios */ suspend fun beginRefundRequestIOS(sku: String): String? /** @@ -5284,17 +5284,17 @@ public interface MutationResolver { * * Returns true if available, false otherwise. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/check-alternative-billing-availability-android + * See: https://openiap.dev/docs/apis/android/check-alternative-billing-availability-android */ suspend fun checkAlternativeBillingAvailabilityAndroid(): Boolean /** * Clear pending transactions in the queue (sandbox helper). - * See: https://www.openiap.dev/docs/apis/ios/clear-transaction-ios + * See: https://openiap.dev/docs/apis/ios/clear-transaction-ios */ suspend fun clearTransactionIOS(): Boolean /** * Consume a consumable purchase so it can be re-bought. - * See: https://www.openiap.dev/docs/apis/android/consume-purchase-android + * See: https://openiap.dev/docs/apis/android/consume-purchase-android */ suspend fun consumePurchaseAndroid(purchaseToken: String): Boolean /** @@ -5304,7 +5304,7 @@ public interface MutationResolver { * * Returns token string, or null if creation failed. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/create-alternative-billing-token-android + * See: https://openiap.dev/docs/apis/android/create-alternative-billing-token-android */ suspend fun createAlternativeBillingTokenAndroid(): String? /** @@ -5313,27 +5313,27 @@ public interface MutationResolver { * * Returns external transaction token needed for reporting external transactions. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/create-billing-program-reporting-details-android + * See: https://openiap.dev/docs/apis/android/create-billing-program-reporting-details-android */ suspend fun createBillingProgramReportingDetailsAndroid(program: BillingProgramAndroid): BillingProgramReportingDetailsAndroid /** * Open the platform's subscription management UI. - * See: https://www.openiap.dev/docs/apis/deep-link-to-subscriptions + * See: https://openiap.dev/docs/apis/deep-link-to-subscriptions */ suspend fun deepLinkToSubscriptions(options: DeepLinkOptions? = null): Unit /** * Close the store connection and release resources. - * See: https://www.openiap.dev/docs/apis/end-connection + * See: https://openiap.dev/docs/apis/end-connection */ suspend fun endConnection(): Boolean /** * Complete a transaction after server-side verification. Required on Android within 3 days. - * See: https://www.openiap.dev/docs/apis/finish-transaction + * See: https://openiap.dev/docs/apis/finish-transaction */ suspend fun finishTransaction(purchase: PurchaseInput, isConsumable: Boolean? = null): Unit /** * Initialize the store connection. Call before any IAP API. - * See: https://www.openiap.dev/docs/apis/init-connection + * See: https://openiap.dev/docs/apis/init-connection */ suspend fun initConnection(config: InitConnectionConfig? = null): Boolean /** @@ -5343,7 +5343,7 @@ public interface MutationResolver { * Available in Google Play Billing Library 8.2.0+. * Returns availability result with isAvailable flag. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/is-billing-program-available-android + * See: https://openiap.dev/docs/apis/android/is-billing-program-available-android */ suspend fun isBillingProgramAvailableAndroid(program: BillingProgramAndroid): BillingProgramAvailabilityResultAndroid /** @@ -5352,29 +5352,29 @@ public interface MutationResolver { * * Shows Play Store dialog and optionally launches external URL. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/launch-external-link-android + * See: https://openiap.dev/docs/apis/android/launch-external-link-android */ suspend fun launchExternalLinkAndroid(params: LaunchExternalLinkParamsAndroid): Boolean /** * Show the App Store offer code redemption sheet. - * See: https://www.openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ suspend fun presentCodeRedemptionSheetIOS(): Boolean /** * Present an external purchase link, StoreKit External (iOS 16+). - * See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-link-ios + * See: https://openiap.dev/docs/apis/ios/present-external-purchase-link-ios */ suspend fun presentExternalPurchaseLinkIOS(url: String): ExternalPurchaseLinkResultIOS /** * Present the external purchase notice sheet (iOS 17.4+). * Uses ExternalPurchase.presentNoticeSheet() which returns a token when the user continues. * Reference: https://developer.apple.com/documentation/storekit/externalpurchase/presentnoticesheet() - * See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios + * See: https://openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios */ suspend fun presentExternalPurchaseNoticeSheetIOS(): ExternalPurchaseNoticeResultIOS /** * Initiate a purchase or subscription flow; rely on events for final state. - * See: https://www.openiap.dev/docs/apis/request-purchase + * See: https://openiap.dev/docs/apis/request-purchase */ suspend fun requestPurchase(params: RequestPurchaseProps): RequestPurchaseResult? /** @@ -5383,12 +5383,12 @@ public interface MutationResolver { * @deprecated Use promotedProductListenerIOS to receive the productId, * then call requestPurchase with that SKU instead. In StoreKit 2, * promoted products can be purchased directly via the standard purchase flow. - * See: https://www.openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios + * See: https://openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios */ suspend fun requestPurchaseOnPromotedProductIOS(): Boolean /** * Restore non-consumable and active subscription purchases. - * See: https://www.openiap.dev/docs/apis/restore-purchases + * See: https://openiap.dev/docs/apis/restore-purchases */ suspend fun restorePurchases(): Unit /** @@ -5397,29 +5397,29 @@ public interface MutationResolver { * * Returns true if user accepted, false if user canceled. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/show-alternative-billing-dialog-android + * See: https://openiap.dev/docs/apis/android/show-alternative-billing-dialog-android */ suspend fun showAlternativeBillingDialogAndroid(): Boolean /** * Present the disclosure sheet required before linking out via ExternalPurchaseCustomLink (iOS 18.1+). * Call this after a deliberate customer interaction before linking out to external purchases. * Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/shownotice(type:) - * See: https://www.openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios + * See: https://openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios */ suspend fun showExternalPurchaseCustomLinkNoticeIOS(noticeType: ExternalPurchaseCustomLinkNoticeTypeIOS): ExternalPurchaseCustomLinkNoticeResultIOS /** * Present the manage-subscriptions sheet and return changed purchases (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/show-manage-subscriptions-ios + * See: https://openiap.dev/docs/apis/ios/show-manage-subscriptions-ios */ suspend fun showManageSubscriptionsIOS(): List /** * Force sync transactions with the App Store (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/sync-ios + * See: https://openiap.dev/docs/apis/ios/sync-ios */ suspend fun syncIOS(): Boolean /** * Deprecated. Validate purchase receipts with the configured providers — use verifyPurchase instead. - * See: https://www.openiap.dev/docs/features/validation#verify-purchase + * See: https://openiap.dev/docs/features/validation#verify-purchase */ suspend fun validateReceipt(options: VerifyPurchaseProps): VerifyPurchaseResult /** @@ -5428,14 +5428,14 @@ public interface MutationResolver { * + receipt/JWS metadata, VerifyPurchaseResultAndroid carries Play Store * receipt fields (no isValid), and VerifyPurchaseResultHorizon uses success. * Inspect the concrete variant before reading fields. - * See: https://www.openiap.dev/docs/features/validation#verify-purchase + * See: https://openiap.dev/docs/features/validation#verify-purchase */ suspend fun verifyPurchase(options: VerifyPurchaseProps): VerifyPurchaseResult /** * Verify via a managed provider without standing up your own server. The * PurchaseVerificationProvider enum currently exposes only IAPKit; platform * availability may differ by implementation. - * See: https://www.openiap.dev/docs/features/validation#verify-purchase-with-provider + * See: https://openiap.dev/docs/features/validation#verify-purchase-with-provider */ suspend fun verifyPurchaseWithProvider(options: VerifyPurchaseWithProviderProps): VerifyPurchaseWithProviderResult } @@ -5447,22 +5447,22 @@ public interface QueryResolver { /** * Check eligibility for the external purchase notice sheet (iOS 17.4+). * Uses ExternalPurchase.canPresent. - * See: https://www.openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios + * See: https://openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios */ suspend fun canPresentExternalPurchaseNoticeIOS(): Boolean /** * Get the user's current entitlement for a product, using StoreKit 2 (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/current-entitlement-ios + * See: https://openiap.dev/docs/apis/ios/current-entitlement-ios */ suspend fun currentEntitlementIOS(sku: String): PurchaseIOS? /** * Fetch products or subscriptions from the store. - * See: https://www.openiap.dev/docs/apis/fetch-products + * See: https://openiap.dev/docs/apis/fetch-products */ suspend fun fetchProducts(params: ProductRequest): FetchProductsResult /** * Get details of all currently active subscriptions (filters by subscriptionIds when provided). - * See: https://www.openiap.dev/docs/apis/get-active-subscriptions + * See: https://openiap.dev/docs/apis/get-active-subscriptions */ suspend fun getActiveSubscriptions(subscriptionIds: List? = null): List /** @@ -5470,91 +5470,91 @@ public interface QueryResolver { * Requires the SK2ConsumableTransactionHistory Info.plist key in the host app * for finished consumables to be included (iOS 18+). * Unlike getAvailablePurchases, always returns the iOS-specific PurchaseIOS shape. - * See: https://www.openiap.dev/docs/apis/ios/get-all-transactions-ios + * See: https://openiap.dev/docs/apis/ios/get-all-transactions-ios */ suspend fun getAllTransactionsIOS(): List /** * Fetch the app transaction (iOS 16+). - * See: https://www.openiap.dev/docs/apis/ios/get-app-transaction-ios + * See: https://openiap.dev/docs/apis/ios/get-app-transaction-ios */ suspend fun getAppTransactionIOS(): AppTransaction? /** * List active purchases for the current user. - * See: https://www.openiap.dev/docs/apis/get-available-purchases + * See: https://openiap.dev/docs/apis/get-available-purchases */ suspend fun getAvailablePurchases(options: PurchaseOptions? = null): List /** * Fetch a token for Apple's External Purchase Server reporting API (iOS 18.1+). * Use this token to report transactions made through ExternalPurchaseCustomLink. * Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) - * See: https://www.openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios + * See: https://openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios */ suspend fun getExternalPurchaseCustomLinkTokenIOS(tokenType: ExternalPurchaseCustomLinkTokenTypeIOS): ExternalPurchaseCustomLinkTokenResultIOS /** * List unfinished StoreKit transactions in the queue. - * See: https://www.openiap.dev/docs/apis/ios/get-pending-transactions-ios + * See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios */ suspend fun getPendingTransactionsIOS(): List /** * Read the App Store-promoted product, if any (iOS 11+). - * See: https://www.openiap.dev/docs/apis/ios/get-promoted-product-ios + * See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios */ suspend fun getPromotedProductIOS(): ProductIOS? /** * Get base64-encoded receipt data (legacy validation). - * See: https://www.openiap.dev/docs/apis/ios/get-receipt-data-ios + * See: https://openiap.dev/docs/apis/ios/get-receipt-data-ios */ suspend fun getReceiptDataIOS(): String? /** * Return the user's storefront country code. - * See: https://www.openiap.dev/docs/apis/get-storefront + * See: https://openiap.dev/docs/apis/get-storefront */ suspend fun getStorefront(): String /** * Deprecated. Get the current App Store storefront country code — use cross-platform getStorefront instead. - * See: https://www.openiap.dev/docs/apis/ios/get-storefront-ios + * See: https://openiap.dev/docs/apis/ios/get-storefront-ios */ suspend fun getStorefrontIOS(): String /** * Return the JWS string for a transaction (StoreKit 2). - * See: https://www.openiap.dev/docs/apis/ios/get-transaction-jws-ios + * See: https://openiap.dev/docs/apis/ios/get-transaction-jws-ios */ suspend fun getTransactionJwsIOS(sku: String): String? /** * Check whether the user has any active subscription. - * See: https://www.openiap.dev/docs/apis/has-active-subscriptions + * See: https://openiap.dev/docs/apis/has-active-subscriptions */ suspend fun hasActiveSubscriptions(subscriptionIds: List? = null): Boolean /** * Check eligibility for the custom-link variant of external purchase (iOS 18.1+). * Returns true if the app can use custom external purchase links. * Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/iseligible - * See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios + * See: https://openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios */ suspend fun isEligibleForExternalPurchaseCustomLinkIOS(): Boolean /** * Check intro-offer eligibility for a subscription group. - * See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios + * See: https://openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios */ suspend fun isEligibleForIntroOfferIOS(groupID: String): Boolean /** * Check whether a transaction's JWS verification passed (StoreKit 2). - * See: https://www.openiap.dev/docs/apis/ios/is-transaction-verified-ios + * See: https://openiap.dev/docs/apis/ios/is-transaction-verified-ios */ suspend fun isTransactionVerifiedIOS(sku: String): Boolean /** * Get the latest verified transaction for a product, using StoreKit 2. - * See: https://www.openiap.dev/docs/apis/ios/latest-transaction-ios + * See: https://openiap.dev/docs/apis/ios/latest-transaction-ios */ suspend fun latestTransactionIOS(sku: String): PurchaseIOS? /** * Get subscription status objects from StoreKit 2 (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/subscription-status-ios + * See: https://openiap.dev/docs/apis/ios/subscription-status-ios */ suspend fun subscriptionStatusIOS(sku: String): List /** * Deprecated. Legacy App Store receipt validation — use verifyPurchase instead. - * See: https://www.openiap.dev/docs/apis/ios/validate-receipt-ios + * See: https://openiap.dev/docs/apis/ios/validate-receipt-ios */ suspend fun validateReceiptIOS(options: VerifyPurchaseProps): VerifyPurchaseResultIOS } diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift index 8cde48d8..b1ac4d36 100644 --- a/packages/gql/src/generated/Types.swift +++ b/packages/gql/src/generated/Types.swift @@ -2502,22 +2502,22 @@ public enum VerifyPurchaseResult: Codable { /// GraphQL root mutation operations. public protocol MutationResolver { /// Acknowledge a non-consumable purchase. Required within 3 days or Google auto-refunds. - /// See: https://www.openiap.dev/docs/apis/android/acknowledge-purchase-android + /// See: https://openiap.dev/docs/apis/android/acknowledge-purchase-android func acknowledgePurchaseAndroid(_ purchaseToken: String) async throws -> Bool /// Present the refund request sheet (iOS 15+). See also Features → Refund. - /// See: https://www.openiap.dev/docs/apis/ios/begin-refund-request-ios + /// See: https://openiap.dev/docs/apis/ios/begin-refund-request-ios func beginRefundRequestIOS(_ sku: String) async throws -> String? /// Check whether alternative billing is available for the user. Step 1 of the alternative billing flow. /// /// Returns true if available, false otherwise. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/check-alternative-billing-availability-android + /// See: https://openiap.dev/docs/apis/android/check-alternative-billing-availability-android func checkAlternativeBillingAvailabilityAndroid() async throws -> Bool /// Clear pending transactions in the queue (sandbox helper). - /// See: https://www.openiap.dev/docs/apis/ios/clear-transaction-ios + /// See: https://openiap.dev/docs/apis/ios/clear-transaction-ios func clearTransactionIOS() async throws -> Bool /// Consume a consumable purchase so it can be re-bought. - /// See: https://www.openiap.dev/docs/apis/android/consume-purchase-android + /// See: https://openiap.dev/docs/apis/android/consume-purchase-android func consumePurchaseAndroid(_ purchaseToken: String) async throws -> Bool /// Create a reporting token for an alternative billing flow. Step 3 of the alternative billing flow. /// Must be called AFTER successful payment in your payment system. @@ -2525,26 +2525,26 @@ public protocol MutationResolver { /// /// Returns token string, or null if creation failed. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/create-alternative-billing-token-android + /// See: https://openiap.dev/docs/apis/android/create-alternative-billing-token-android func createAlternativeBillingTokenAndroid() async throws -> String? /// Create the reporting payload Google requires after a Developer-Provided Billing transaction (Play Billing 8.3.0+). /// Replaces the deprecated createExternalOfferReportingDetailsAsync API. /// /// Returns external transaction token needed for reporting external transactions. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/create-billing-program-reporting-details-android + /// See: https://openiap.dev/docs/apis/android/create-billing-program-reporting-details-android func createBillingProgramReportingDetailsAndroid(_ program: BillingProgramAndroid) async throws -> BillingProgramReportingDetailsAndroid /// Open the platform's subscription management UI. - /// See: https://www.openiap.dev/docs/apis/deep-link-to-subscriptions + /// See: https://openiap.dev/docs/apis/deep-link-to-subscriptions func deepLinkToSubscriptions(_ options: DeepLinkOptions?) async throws -> Void /// Close the store connection and release resources. - /// See: https://www.openiap.dev/docs/apis/end-connection + /// See: https://openiap.dev/docs/apis/end-connection func endConnection() async throws -> Bool /// Complete a transaction after server-side verification. Required on Android within 3 days. - /// See: https://www.openiap.dev/docs/apis/finish-transaction + /// See: https://openiap.dev/docs/apis/finish-transaction func finishTransaction(purchase: PurchaseInput, isConsumable: Bool?) async throws -> Void /// Initialize the store connection. Call before any IAP API. - /// See: https://www.openiap.dev/docs/apis/init-connection + /// See: https://openiap.dev/docs/apis/init-connection func initConnection(_ config: InitConnectionConfig?) async throws -> Bool /// Check whether a billing program (e.g., External Payments) is available for the current user. /// Replaces the deprecated isExternalOfferAvailableAsync API. @@ -2552,71 +2552,71 @@ public protocol MutationResolver { /// Available in Google Play Billing Library 8.2.0+. /// Returns availability result with isAvailable flag. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/is-billing-program-available-android + /// See: https://openiap.dev/docs/apis/android/is-billing-program-available-android func isBillingProgramAvailableAndroid(_ program: BillingProgramAndroid) async throws -> BillingProgramAvailabilityResultAndroid /// Launch an external content/offer link from inside the Billing Programs flow (Play Billing 8.2.0+). /// Replaces the deprecated showExternalOfferInformationDialog API. /// /// Shows Play Store dialog and optionally launches external URL. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/launch-external-link-android + /// See: https://openiap.dev/docs/apis/android/launch-external-link-android func launchExternalLinkAndroid(_ params: LaunchExternalLinkParamsAndroid) async throws -> Bool /// Show the App Store offer code redemption sheet. - /// See: https://www.openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios func presentCodeRedemptionSheetIOS() async throws -> Bool /// Present an external purchase link, StoreKit External (iOS 16+). - /// See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-link-ios + /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-link-ios func presentExternalPurchaseLinkIOS(_ url: String) async throws -> ExternalPurchaseLinkResultIOS /// Present the external purchase notice sheet (iOS 17.4+). /// Uses ExternalPurchase.presentNoticeSheet() which returns a token when the user continues. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchase/presentnoticesheet() - /// See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios + /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios func presentExternalPurchaseNoticeSheetIOS() async throws -> ExternalPurchaseNoticeResultIOS /// Initiate a purchase or subscription flow; rely on events for final state. - /// See: https://www.openiap.dev/docs/apis/request-purchase + /// See: https://openiap.dev/docs/apis/request-purchase func requestPurchase(_ params: RequestPurchaseProps) async throws -> RequestPurchaseResult? /// Buy the currently promoted product. /// /// @deprecated Use promotedProductListenerIOS to receive the productId, /// then call requestPurchase with that SKU instead. In StoreKit 2, /// promoted products can be purchased directly via the standard purchase flow. - /// See: https://www.openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios + /// See: https://openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios func requestPurchaseOnPromotedProductIOS() async throws -> Bool /// Restore non-consumable and active subscription purchases. - /// See: https://www.openiap.dev/docs/apis/restore-purchases + /// See: https://openiap.dev/docs/apis/restore-purchases func restorePurchases() async throws -> Void /// Display Google's alternative billing information dialog. Step 2 of the alternative billing flow. /// Must be called BEFORE processing payment in your payment system. /// /// Returns true if user accepted, false if user canceled. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/show-alternative-billing-dialog-android + /// See: https://openiap.dev/docs/apis/android/show-alternative-billing-dialog-android func showAlternativeBillingDialogAndroid() async throws -> Bool /// Present the disclosure sheet required before linking out via ExternalPurchaseCustomLink (iOS 18.1+). /// Call this after a deliberate customer interaction before linking out to external purchases. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/shownotice(type:) - /// See: https://www.openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios + /// See: https://openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios func showExternalPurchaseCustomLinkNoticeIOS(_ noticeType: ExternalPurchaseCustomLinkNoticeTypeIOS) async throws -> ExternalPurchaseCustomLinkNoticeResultIOS /// Present the manage-subscriptions sheet and return changed purchases (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/show-manage-subscriptions-ios + /// See: https://openiap.dev/docs/apis/ios/show-manage-subscriptions-ios func showManageSubscriptionsIOS() async throws -> [PurchaseIOS] /// Force sync transactions with the App Store (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/sync-ios + /// See: https://openiap.dev/docs/apis/ios/sync-ios func syncIOS() async throws -> Bool /// Deprecated. Validate purchase receipts with the configured providers — use verifyPurchase instead. - /// See: https://www.openiap.dev/docs/features/validation#verify-purchase + /// See: https://openiap.dev/docs/features/validation#verify-purchase func validateReceipt(_ options: VerifyPurchaseProps) async throws -> VerifyPurchaseResult /// Verify a purchase against your own backend. Returns a platform-specific /// variant of VerifyPurchaseResult — VerifyPurchaseResultIOS exposes isValid /// + receipt/JWS metadata, VerifyPurchaseResultAndroid carries Play Store /// receipt fields (no isValid), and VerifyPurchaseResultHorizon uses success. /// Inspect the concrete variant before reading fields. - /// See: https://www.openiap.dev/docs/features/validation#verify-purchase + /// See: https://openiap.dev/docs/features/validation#verify-purchase func verifyPurchase(_ options: VerifyPurchaseProps) async throws -> VerifyPurchaseResult /// Verify via a managed provider without standing up your own server. The /// PurchaseVerificationProvider enum currently exposes only IAPKit; platform /// availability may differ by implementation. - /// See: https://www.openiap.dev/docs/features/validation#verify-purchase-with-provider + /// See: https://openiap.dev/docs/features/validation#verify-purchase-with-provider func verifyPurchaseWithProvider(_ options: VerifyPurchaseWithProviderProps) async throws -> VerifyPurchaseWithProviderResult } @@ -2624,74 +2624,74 @@ public protocol MutationResolver { public protocol QueryResolver { /// Check eligibility for the external purchase notice sheet (iOS 17.4+). /// Uses ExternalPurchase.canPresent. - /// See: https://www.openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios + /// See: https://openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios func canPresentExternalPurchaseNoticeIOS() async throws -> Bool /// Get the user's current entitlement for a product, using StoreKit 2 (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/current-entitlement-ios + /// See: https://openiap.dev/docs/apis/ios/current-entitlement-ios func currentEntitlementIOS(_ sku: String) async throws -> PurchaseIOS? /// Fetch products or subscriptions from the store. - /// See: https://www.openiap.dev/docs/apis/fetch-products + /// See: https://openiap.dev/docs/apis/fetch-products func fetchProducts(_ params: ProductRequest) async throws -> FetchProductsResult /// Get details of all currently active subscriptions (filters by subscriptionIds when provided). - /// See: https://www.openiap.dev/docs/apis/get-active-subscriptions + /// See: https://openiap.dev/docs/apis/get-active-subscriptions func getActiveSubscriptions(_ subscriptionIds: [String]?) async throws -> [ActiveSubscription] /// List every StoreKit transaction (finished + unfinished) for the current user. /// Requires the SK2ConsumableTransactionHistory Info.plist key in the host app /// for finished consumables to be included (iOS 18+). /// Unlike getAvailablePurchases, always returns the iOS-specific PurchaseIOS shape. - /// See: https://www.openiap.dev/docs/apis/ios/get-all-transactions-ios + /// See: https://openiap.dev/docs/apis/ios/get-all-transactions-ios func getAllTransactionsIOS() async throws -> [PurchaseIOS] /// Fetch the app transaction (iOS 16+). - /// See: https://www.openiap.dev/docs/apis/ios/get-app-transaction-ios + /// See: https://openiap.dev/docs/apis/ios/get-app-transaction-ios func getAppTransactionIOS() async throws -> AppTransaction? /// List active purchases for the current user. - /// See: https://www.openiap.dev/docs/apis/get-available-purchases + /// See: https://openiap.dev/docs/apis/get-available-purchases func getAvailablePurchases(_ options: PurchaseOptions?) async throws -> [Purchase] /// Fetch a token for Apple's External Purchase Server reporting API (iOS 18.1+). /// Use this token to report transactions made through ExternalPurchaseCustomLink. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) - /// See: https://www.openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios + /// See: https://openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios func getExternalPurchaseCustomLinkTokenIOS(_ tokenType: ExternalPurchaseCustomLinkTokenTypeIOS) async throws -> ExternalPurchaseCustomLinkTokenResultIOS /// List unfinished StoreKit transactions in the queue. - /// See: https://www.openiap.dev/docs/apis/ios/get-pending-transactions-ios + /// See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios func getPendingTransactionsIOS() async throws -> [PurchaseIOS] /// Read the App Store-promoted product, if any (iOS 11+). - /// See: https://www.openiap.dev/docs/apis/ios/get-promoted-product-ios + /// See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios func getPromotedProductIOS() async throws -> ProductIOS? /// Get base64-encoded receipt data (legacy validation). - /// See: https://www.openiap.dev/docs/apis/ios/get-receipt-data-ios + /// See: https://openiap.dev/docs/apis/ios/get-receipt-data-ios func getReceiptDataIOS() async throws -> String? /// Return the user's storefront country code. - /// See: https://www.openiap.dev/docs/apis/get-storefront + /// See: https://openiap.dev/docs/apis/get-storefront func getStorefront() async throws -> String /// Deprecated. Get the current App Store storefront country code — use cross-platform getStorefront instead. - /// See: https://www.openiap.dev/docs/apis/ios/get-storefront-ios + /// See: https://openiap.dev/docs/apis/ios/get-storefront-ios func getStorefrontIOS() async throws -> String /// Return the JWS string for a transaction (StoreKit 2). - /// See: https://www.openiap.dev/docs/apis/ios/get-transaction-jws-ios + /// See: https://openiap.dev/docs/apis/ios/get-transaction-jws-ios func getTransactionJwsIOS(_ sku: String) async throws -> String? /// Check whether the user has any active subscription. - /// See: https://www.openiap.dev/docs/apis/has-active-subscriptions + /// See: https://openiap.dev/docs/apis/has-active-subscriptions func hasActiveSubscriptions(_ subscriptionIds: [String]?) async throws -> Bool /// Check eligibility for the custom-link variant of external purchase (iOS 18.1+). /// Returns true if the app can use custom external purchase links. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/iseligible - /// See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios + /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios func isEligibleForExternalPurchaseCustomLinkIOS() async throws -> Bool /// Check intro-offer eligibility for a subscription group. - /// See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios + /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios func isEligibleForIntroOfferIOS(_ groupID: String) async throws -> Bool /// Check whether a transaction's JWS verification passed (StoreKit 2). - /// See: https://www.openiap.dev/docs/apis/ios/is-transaction-verified-ios + /// See: https://openiap.dev/docs/apis/ios/is-transaction-verified-ios func isTransactionVerifiedIOS(_ sku: String) async throws -> Bool /// Get the latest verified transaction for a product, using StoreKit 2. - /// See: https://www.openiap.dev/docs/apis/ios/latest-transaction-ios + /// See: https://openiap.dev/docs/apis/ios/latest-transaction-ios func latestTransactionIOS(_ sku: String) async throws -> PurchaseIOS? /// Get subscription status objects from StoreKit 2 (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/subscription-status-ios + /// See: https://openiap.dev/docs/apis/ios/subscription-status-ios func subscriptionStatusIOS(_ sku: String) async throws -> [SubscriptionStatusIOS] /// Deprecated. Legacy App Store receipt validation — use verifyPurchase instead. - /// See: https://www.openiap.dev/docs/apis/ios/validate-receipt-ios + /// See: https://openiap.dev/docs/apis/ios/validate-receipt-ios func validateReceiptIOS(_ options: VerifyPurchaseProps) async throws -> VerifyPurchaseResultIOS } diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index 2558620f..2e08ac0a 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -5233,22 +5233,22 @@ sealed class VerifyPurchaseResult { /// GraphQL root mutation operations. abstract class MutationResolver { /// Acknowledge a non-consumable purchase. Required within 3 days or Google auto-refunds. - /// See: https://www.openiap.dev/docs/apis/android/acknowledge-purchase-android + /// See: https://openiap.dev/docs/apis/android/acknowledge-purchase-android Future acknowledgePurchaseAndroid(String purchaseToken); /// Present the refund request sheet (iOS 15+). See also Features → Refund. - /// See: https://www.openiap.dev/docs/apis/ios/begin-refund-request-ios + /// See: https://openiap.dev/docs/apis/ios/begin-refund-request-ios Future beginRefundRequestIOS(String sku); /// Check whether alternative billing is available for the user. Step 1 of the alternative billing flow. /// /// Returns true if available, false otherwise. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/check-alternative-billing-availability-android + /// See: https://openiap.dev/docs/apis/android/check-alternative-billing-availability-android Future checkAlternativeBillingAvailabilityAndroid(); /// Clear pending transactions in the queue (sandbox helper). - /// See: https://www.openiap.dev/docs/apis/ios/clear-transaction-ios + /// See: https://openiap.dev/docs/apis/ios/clear-transaction-ios Future clearTransactionIOS(); /// Consume a consumable purchase so it can be re-bought. - /// See: https://www.openiap.dev/docs/apis/android/consume-purchase-android + /// See: https://openiap.dev/docs/apis/android/consume-purchase-android Future consumePurchaseAndroid(String purchaseToken); /// Create a reporting token for an alternative billing flow. Step 3 of the alternative billing flow. /// Must be called AFTER successful payment in your payment system. @@ -5256,32 +5256,32 @@ abstract class MutationResolver { /// /// Returns token string, or null if creation failed. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/create-alternative-billing-token-android + /// See: https://openiap.dev/docs/apis/android/create-alternative-billing-token-android Future createAlternativeBillingTokenAndroid(); /// Create the reporting payload Google requires after a Developer-Provided Billing transaction (Play Billing 8.3.0+). /// Replaces the deprecated createExternalOfferReportingDetailsAsync API. /// /// Returns external transaction token needed for reporting external transactions. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/create-billing-program-reporting-details-android + /// See: https://openiap.dev/docs/apis/android/create-billing-program-reporting-details-android Future createBillingProgramReportingDetailsAndroid(BillingProgramAndroid program); /// Open the platform's subscription management UI. - /// See: https://www.openiap.dev/docs/apis/deep-link-to-subscriptions + /// See: https://openiap.dev/docs/apis/deep-link-to-subscriptions Future deepLinkToSubscriptions({ String? packageNameAndroid, String? skuAndroid, }); /// Close the store connection and release resources. - /// See: https://www.openiap.dev/docs/apis/end-connection + /// See: https://openiap.dev/docs/apis/end-connection Future endConnection(); /// Complete a transaction after server-side verification. Required on Android within 3 days. - /// See: https://www.openiap.dev/docs/apis/finish-transaction + /// See: https://openiap.dev/docs/apis/finish-transaction Future finishTransaction({ required PurchaseInput purchase, bool? isConsumable, }); /// Initialize the store connection. Call before any IAP API. - /// See: https://www.openiap.dev/docs/apis/init-connection + /// See: https://openiap.dev/docs/apis/init-connection Future initConnection({ AlternativeBillingModeAndroid? alternativeBillingModeAndroid, BillingProgramAndroid? enableBillingProgramAndroid, @@ -5292,14 +5292,14 @@ abstract class MutationResolver { /// Available in Google Play Billing Library 8.2.0+. /// Returns availability result with isAvailable flag. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/is-billing-program-available-android + /// See: https://openiap.dev/docs/apis/android/is-billing-program-available-android Future isBillingProgramAvailableAndroid(BillingProgramAndroid program); /// Launch an external content/offer link from inside the Billing Programs flow (Play Billing 8.2.0+). /// Replaces the deprecated showExternalOfferInformationDialog API. /// /// Shows Play Store dialog and optionally launches external URL. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/launch-external-link-android + /// See: https://openiap.dev/docs/apis/android/launch-external-link-android Future launchExternalLinkAndroid({ required BillingProgramAndroid billingProgram, required ExternalLinkLaunchModeAndroid launchMode, @@ -5307,49 +5307,49 @@ abstract class MutationResolver { required String linkUri, }); /// Show the App Store offer code redemption sheet. - /// See: https://www.openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios Future presentCodeRedemptionSheetIOS(); /// Present an external purchase link, StoreKit External (iOS 16+). - /// See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-link-ios + /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-link-ios Future presentExternalPurchaseLinkIOS(String url); /// Present the external purchase notice sheet (iOS 17.4+). /// Uses ExternalPurchase.presentNoticeSheet() which returns a token when the user continues. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchase/presentnoticesheet() - /// See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios + /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios Future presentExternalPurchaseNoticeSheetIOS(); /// Initiate a purchase or subscription flow; rely on events for final state. - /// See: https://www.openiap.dev/docs/apis/request-purchase + /// See: https://openiap.dev/docs/apis/request-purchase Future requestPurchase(RequestPurchaseProps params); /// Buy the currently promoted product. /// /// @deprecated Use promotedProductListenerIOS to receive the productId, /// then call requestPurchase with that SKU instead. In StoreKit 2, /// promoted products can be purchased directly via the standard purchase flow. - /// See: https://www.openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios + /// See: https://openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios Future requestPurchaseOnPromotedProductIOS(); /// Restore non-consumable and active subscription purchases. - /// See: https://www.openiap.dev/docs/apis/restore-purchases + /// See: https://openiap.dev/docs/apis/restore-purchases Future restorePurchases(); /// Display Google's alternative billing information dialog. Step 2 of the alternative billing flow. /// Must be called BEFORE processing payment in your payment system. /// /// Returns true if user accepted, false if user canceled. /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://www.openiap.dev/docs/apis/android/show-alternative-billing-dialog-android + /// See: https://openiap.dev/docs/apis/android/show-alternative-billing-dialog-android Future showAlternativeBillingDialogAndroid(); /// Present the disclosure sheet required before linking out via ExternalPurchaseCustomLink (iOS 18.1+). /// Call this after a deliberate customer interaction before linking out to external purchases. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/shownotice(type:) - /// See: https://www.openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios + /// See: https://openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios Future showExternalPurchaseCustomLinkNoticeIOS(ExternalPurchaseCustomLinkNoticeTypeIOS noticeType); /// Present the manage-subscriptions sheet and return changed purchases (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/show-manage-subscriptions-ios + /// See: https://openiap.dev/docs/apis/ios/show-manage-subscriptions-ios Future> showManageSubscriptionsIOS(); /// Force sync transactions with the App Store (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/sync-ios + /// See: https://openiap.dev/docs/apis/ios/sync-ios Future syncIOS(); /// Deprecated. Validate purchase receipts with the configured providers — use verifyPurchase instead. - /// See: https://www.openiap.dev/docs/features/validation#verify-purchase + /// See: https://openiap.dev/docs/features/validation#verify-purchase Future validateReceipt({ VerifyPurchaseAppleOptions? apple, VerifyPurchaseGoogleOptions? google, @@ -5360,7 +5360,7 @@ abstract class MutationResolver { /// + receipt/JWS metadata, VerifyPurchaseResultAndroid carries Play Store /// receipt fields (no isValid), and VerifyPurchaseResultHorizon uses success. /// Inspect the concrete variant before reading fields. - /// See: https://www.openiap.dev/docs/features/validation#verify-purchase + /// See: https://openiap.dev/docs/features/validation#verify-purchase Future verifyPurchase({ VerifyPurchaseAppleOptions? apple, VerifyPurchaseGoogleOptions? google, @@ -5369,7 +5369,7 @@ abstract class MutationResolver { /// Verify via a managed provider without standing up your own server. The /// PurchaseVerificationProvider enum currently exposes only IAPKit; platform /// availability may differ by implementation. - /// See: https://www.openiap.dev/docs/features/validation#verify-purchase-with-provider + /// See: https://openiap.dev/docs/features/validation#verify-purchase-with-provider Future verifyPurchaseWithProvider({ RequestVerifyPurchaseWithIapkitProps? iapkit, required PurchaseVerificationProvider provider, @@ -5380,31 +5380,31 @@ abstract class MutationResolver { abstract class QueryResolver { /// Check eligibility for the external purchase notice sheet (iOS 17.4+). /// Uses ExternalPurchase.canPresent. - /// See: https://www.openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios + /// See: https://openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios Future canPresentExternalPurchaseNoticeIOS(); /// Get the user's current entitlement for a product, using StoreKit 2 (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/current-entitlement-ios + /// See: https://openiap.dev/docs/apis/ios/current-entitlement-ios Future currentEntitlementIOS(String sku); /// Fetch products or subscriptions from the store. - /// See: https://www.openiap.dev/docs/apis/fetch-products + /// See: https://openiap.dev/docs/apis/fetch-products Future fetchProducts({ required List skus, ProductQueryType? type, }); /// Get details of all currently active subscriptions (filters by subscriptionIds when provided). - /// See: https://www.openiap.dev/docs/apis/get-active-subscriptions + /// See: https://openiap.dev/docs/apis/get-active-subscriptions Future> getActiveSubscriptions([List? subscriptionIds]); /// List every StoreKit transaction (finished + unfinished) for the current user. /// Requires the SK2ConsumableTransactionHistory Info.plist key in the host app /// for finished consumables to be included (iOS 18+). /// Unlike getAvailablePurchases, always returns the iOS-specific PurchaseIOS shape. - /// See: https://www.openiap.dev/docs/apis/ios/get-all-transactions-ios + /// See: https://openiap.dev/docs/apis/ios/get-all-transactions-ios Future> getAllTransactionsIOS(); /// Fetch the app transaction (iOS 16+). - /// See: https://www.openiap.dev/docs/apis/ios/get-app-transaction-ios + /// See: https://openiap.dev/docs/apis/ios/get-app-transaction-ios Future getAppTransactionIOS(); /// List active purchases for the current user. - /// See: https://www.openiap.dev/docs/apis/get-available-purchases + /// See: https://openiap.dev/docs/apis/get-available-purchases Future> getAvailablePurchases({ bool? alsoPublishToEventListenerIOS, bool? includeSuspendedAndroid, @@ -5413,48 +5413,48 @@ abstract class QueryResolver { /// Fetch a token for Apple's External Purchase Server reporting API (iOS 18.1+). /// Use this token to report transactions made through ExternalPurchaseCustomLink. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) - /// See: https://www.openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios + /// See: https://openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios Future getExternalPurchaseCustomLinkTokenIOS(ExternalPurchaseCustomLinkTokenTypeIOS tokenType); /// List unfinished StoreKit transactions in the queue. - /// See: https://www.openiap.dev/docs/apis/ios/get-pending-transactions-ios + /// See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios Future> getPendingTransactionsIOS(); /// Read the App Store-promoted product, if any (iOS 11+). - /// See: https://www.openiap.dev/docs/apis/ios/get-promoted-product-ios + /// See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios Future getPromotedProductIOS(); /// Get base64-encoded receipt data (legacy validation). - /// See: https://www.openiap.dev/docs/apis/ios/get-receipt-data-ios + /// See: https://openiap.dev/docs/apis/ios/get-receipt-data-ios Future getReceiptDataIOS(); /// Return the user's storefront country code. - /// See: https://www.openiap.dev/docs/apis/get-storefront + /// See: https://openiap.dev/docs/apis/get-storefront Future getStorefront(); /// Deprecated. Get the current App Store storefront country code — use cross-platform getStorefront instead. - /// See: https://www.openiap.dev/docs/apis/ios/get-storefront-ios + /// See: https://openiap.dev/docs/apis/ios/get-storefront-ios Future getStorefrontIOS(); /// Return the JWS string for a transaction (StoreKit 2). - /// See: https://www.openiap.dev/docs/apis/ios/get-transaction-jws-ios + /// See: https://openiap.dev/docs/apis/ios/get-transaction-jws-ios Future getTransactionJwsIOS(String sku); /// Check whether the user has any active subscription. - /// See: https://www.openiap.dev/docs/apis/has-active-subscriptions + /// See: https://openiap.dev/docs/apis/has-active-subscriptions Future hasActiveSubscriptions([List? subscriptionIds]); /// Check eligibility for the custom-link variant of external purchase (iOS 18.1+). /// Returns true if the app can use custom external purchase links. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/iseligible - /// See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios + /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios Future isEligibleForExternalPurchaseCustomLinkIOS(); /// Check intro-offer eligibility for a subscription group. - /// See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios + /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios Future isEligibleForIntroOfferIOS(String groupID); /// Check whether a transaction's JWS verification passed (StoreKit 2). - /// See: https://www.openiap.dev/docs/apis/ios/is-transaction-verified-ios + /// See: https://openiap.dev/docs/apis/ios/is-transaction-verified-ios Future isTransactionVerifiedIOS(String sku); /// Get the latest verified transaction for a product, using StoreKit 2. - /// See: https://www.openiap.dev/docs/apis/ios/latest-transaction-ios + /// See: https://openiap.dev/docs/apis/ios/latest-transaction-ios Future latestTransactionIOS(String sku); /// Get subscription status objects from StoreKit 2 (iOS 15+). - /// See: https://www.openiap.dev/docs/apis/ios/subscription-status-ios + /// See: https://openiap.dev/docs/apis/ios/subscription-status-ios Future> subscriptionStatusIOS(String sku); /// Deprecated. Legacy App Store receipt validation — use verifyPurchase instead. - /// See: https://www.openiap.dev/docs/apis/ios/validate-receipt-ios + /// See: https://openiap.dev/docs/apis/ios/validate-receipt-ios Future validateReceiptIOS({ VerifyPurchaseAppleOptions? apple, VerifyPurchaseGoogleOptions? google, diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts index fc6fef35..fcce2ade 100644 --- a/packages/gql/src/generated/types.ts +++ b/packages/gql/src/generated/types.ts @@ -587,12 +587,12 @@ export interface LimitedQuantityInfoAndroid { export interface Mutation { /** * Acknowledge a non-consumable purchase. Required within 3 days or Google auto-refunds. - * See: https://www.openiap.dev/docs/apis/android/acknowledge-purchase-android + * See: https://openiap.dev/docs/apis/android/acknowledge-purchase-android */ acknowledgePurchaseAndroid: Promise; /** * Present the refund request sheet (iOS 15+). See also Features → Refund. - * See: https://www.openiap.dev/docs/apis/ios/begin-refund-request-ios + * See: https://openiap.dev/docs/apis/ios/begin-refund-request-ios */ beginRefundRequestIOS?: Promise<(string | null)>; /** @@ -600,17 +600,17 @@ export interface Mutation { * * Returns true if available, false otherwise. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/check-alternative-billing-availability-android + * See: https://openiap.dev/docs/apis/android/check-alternative-billing-availability-android */ checkAlternativeBillingAvailabilityAndroid: Promise; /** * Clear pending transactions in the queue (sandbox helper). - * See: https://www.openiap.dev/docs/apis/ios/clear-transaction-ios + * See: https://openiap.dev/docs/apis/ios/clear-transaction-ios */ clearTransactionIOS: Promise; /** * Consume a consumable purchase so it can be re-bought. - * See: https://www.openiap.dev/docs/apis/android/consume-purchase-android + * See: https://openiap.dev/docs/apis/android/consume-purchase-android */ consumePurchaseAndroid: Promise; /** @@ -620,7 +620,7 @@ export interface Mutation { * * Returns token string, or null if creation failed. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/create-alternative-billing-token-android + * See: https://openiap.dev/docs/apis/android/create-alternative-billing-token-android */ createAlternativeBillingTokenAndroid?: Promise<(string | null)>; /** @@ -629,27 +629,27 @@ export interface Mutation { * * Returns external transaction token needed for reporting external transactions. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/create-billing-program-reporting-details-android + * See: https://openiap.dev/docs/apis/android/create-billing-program-reporting-details-android */ createBillingProgramReportingDetailsAndroid: Promise; /** * Open the platform's subscription management UI. - * See: https://www.openiap.dev/docs/apis/deep-link-to-subscriptions + * See: https://openiap.dev/docs/apis/deep-link-to-subscriptions */ deepLinkToSubscriptions: Promise; /** * Close the store connection and release resources. - * See: https://www.openiap.dev/docs/apis/end-connection + * See: https://openiap.dev/docs/apis/end-connection */ endConnection: Promise; /** * Complete a transaction after server-side verification. Required on Android within 3 days. - * See: https://www.openiap.dev/docs/apis/finish-transaction + * See: https://openiap.dev/docs/apis/finish-transaction */ finishTransaction: Promise; /** * Initialize the store connection. Call before any IAP API. - * See: https://www.openiap.dev/docs/apis/init-connection + * See: https://openiap.dev/docs/apis/init-connection */ initConnection: Promise; /** @@ -659,7 +659,7 @@ export interface Mutation { * Available in Google Play Billing Library 8.2.0+. * Returns availability result with isAvailable flag. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/is-billing-program-available-android + * See: https://openiap.dev/docs/apis/android/is-billing-program-available-android */ isBillingProgramAvailableAndroid: Promise; /** @@ -668,29 +668,29 @@ export interface Mutation { * * Shows Play Store dialog and optionally launches external URL. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/launch-external-link-android + * See: https://openiap.dev/docs/apis/android/launch-external-link-android */ launchExternalLinkAndroid: Promise; /** * Show the App Store offer code redemption sheet. - * See: https://www.openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ presentCodeRedemptionSheetIOS: Promise; /** * Present an external purchase link, StoreKit External (iOS 16+). - * See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-link-ios + * See: https://openiap.dev/docs/apis/ios/present-external-purchase-link-ios */ presentExternalPurchaseLinkIOS: Promise; /** * Present the external purchase notice sheet (iOS 17.4+). * Uses ExternalPurchase.presentNoticeSheet() which returns a token when the user continues. * Reference: https://developer.apple.com/documentation/storekit/externalpurchase/presentnoticesheet() - * See: https://www.openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios + * See: https://openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios */ presentExternalPurchaseNoticeSheetIOS: Promise; /** * Initiate a purchase or subscription flow; rely on events for final state. - * See: https://www.openiap.dev/docs/apis/request-purchase + * See: https://openiap.dev/docs/apis/request-purchase */ requestPurchase?: Promise<(Purchase | Purchase[] | null)>; /** @@ -699,13 +699,13 @@ export interface Mutation { * @deprecated Use promotedProductListenerIOS to receive the productId, * then call requestPurchase with that SKU instead. In StoreKit 2, * promoted products can be purchased directly via the standard purchase flow. - * See: https://www.openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios + * See: https://openiap.dev/docs/apis/ios/request-purchase-on-promoted-product-ios * @deprecated Use promotedProductListenerIOS + requestPurchase instead */ requestPurchaseOnPromotedProductIOS: Promise; /** * Restore non-consumable and active subscription purchases. - * See: https://www.openiap.dev/docs/apis/restore-purchases + * See: https://openiap.dev/docs/apis/restore-purchases */ restorePurchases: Promise; /** @@ -714,29 +714,29 @@ export interface Mutation { * * Returns true if user accepted, false if user canceled. * Throws OpenIapError.NotPrepared if billing client not ready. - * See: https://www.openiap.dev/docs/apis/android/show-alternative-billing-dialog-android + * See: https://openiap.dev/docs/apis/android/show-alternative-billing-dialog-android */ showAlternativeBillingDialogAndroid: Promise; /** * Present the disclosure sheet required before linking out via ExternalPurchaseCustomLink (iOS 18.1+). * Call this after a deliberate customer interaction before linking out to external purchases. * Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/shownotice(type:) - * See: https://www.openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios + * See: https://openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios */ showExternalPurchaseCustomLinkNoticeIOS: Promise; /** * Present the manage-subscriptions sheet and return changed purchases (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/show-manage-subscriptions-ios + * See: https://openiap.dev/docs/apis/ios/show-manage-subscriptions-ios */ showManageSubscriptionsIOS: Promise; /** * Force sync transactions with the App Store (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/sync-ios + * See: https://openiap.dev/docs/apis/ios/sync-ios */ syncIOS: Promise; /** * Deprecated. Validate purchase receipts with the configured providers — use verifyPurchase instead. - * See: https://www.openiap.dev/docs/features/validation#verify-purchase + * See: https://openiap.dev/docs/features/validation#verify-purchase * @deprecated Use verifyPurchase */ validateReceipt: Promise; @@ -746,14 +746,14 @@ export interface Mutation { * + receipt/JWS metadata, VerifyPurchaseResultAndroid carries Play Store * receipt fields (no isValid), and VerifyPurchaseResultHorizon uses success. * Inspect the concrete variant before reading fields. - * See: https://www.openiap.dev/docs/features/validation#verify-purchase + * See: https://openiap.dev/docs/features/validation#verify-purchase */ verifyPurchase: Promise; /** * Verify via a managed provider without standing up your own server. The * PurchaseVerificationProvider enum currently exposes only IAPKit; platform * availability may differ by implementation. - * See: https://www.openiap.dev/docs/features/validation#verify-purchase-with-provider + * See: https://openiap.dev/docs/features/validation#verify-purchase-with-provider */ verifyPurchaseWithProvider: Promise; } @@ -1307,22 +1307,22 @@ export interface Query { /** * Check eligibility for the external purchase notice sheet (iOS 17.4+). * Uses ExternalPurchase.canPresent. - * See: https://www.openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios + * See: https://openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios */ canPresentExternalPurchaseNoticeIOS: Promise; /** * Get the user's current entitlement for a product, using StoreKit 2 (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/current-entitlement-ios + * See: https://openiap.dev/docs/apis/ios/current-entitlement-ios */ currentEntitlementIOS?: Promise<(PurchaseIOS | null)>; /** * Fetch products or subscriptions from the store. - * See: https://www.openiap.dev/docs/apis/fetch-products + * See: https://openiap.dev/docs/apis/fetch-products */ fetchProducts: Promise<(ProductOrSubscription[] | Product[] | ProductSubscription[] | null)>; /** * Get details of all currently active subscriptions (filters by subscriptionIds when provided). - * See: https://www.openiap.dev/docs/apis/get-active-subscriptions + * See: https://openiap.dev/docs/apis/get-active-subscriptions */ getActiveSubscriptions: Promise; /** @@ -1330,92 +1330,92 @@ export interface Query { * Requires the SK2ConsumableTransactionHistory Info.plist key in the host app * for finished consumables to be included (iOS 18+). * Unlike getAvailablePurchases, always returns the iOS-specific PurchaseIOS shape. - * See: https://www.openiap.dev/docs/apis/ios/get-all-transactions-ios + * See: https://openiap.dev/docs/apis/ios/get-all-transactions-ios */ getAllTransactionsIOS: Promise; /** * Fetch the app transaction (iOS 16+). - * See: https://www.openiap.dev/docs/apis/ios/get-app-transaction-ios + * See: https://openiap.dev/docs/apis/ios/get-app-transaction-ios */ getAppTransactionIOS?: Promise<(AppTransaction | null)>; /** * List active purchases for the current user. - * See: https://www.openiap.dev/docs/apis/get-available-purchases + * See: https://openiap.dev/docs/apis/get-available-purchases */ getAvailablePurchases: Promise; /** * Fetch a token for Apple's External Purchase Server reporting API (iOS 18.1+). * Use this token to report transactions made through ExternalPurchaseCustomLink. * Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) - * See: https://www.openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios + * See: https://openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios */ getExternalPurchaseCustomLinkTokenIOS: Promise; /** * List unfinished StoreKit transactions in the queue. - * See: https://www.openiap.dev/docs/apis/ios/get-pending-transactions-ios + * See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios */ getPendingTransactionsIOS: Promise; /** * Read the App Store-promoted product, if any (iOS 11+). - * See: https://www.openiap.dev/docs/apis/ios/get-promoted-product-ios + * See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios */ getPromotedProductIOS?: Promise<(ProductIOS | null)>; /** * Get base64-encoded receipt data (legacy validation). - * See: https://www.openiap.dev/docs/apis/ios/get-receipt-data-ios + * See: https://openiap.dev/docs/apis/ios/get-receipt-data-ios */ getReceiptDataIOS?: Promise<(string | null)>; /** * Return the user's storefront country code. - * See: https://www.openiap.dev/docs/apis/get-storefront + * See: https://openiap.dev/docs/apis/get-storefront */ getStorefront: Promise; /** * Deprecated. Get the current App Store storefront country code — use cross-platform getStorefront instead. - * See: https://www.openiap.dev/docs/apis/ios/get-storefront-ios + * See: https://openiap.dev/docs/apis/ios/get-storefront-ios * @deprecated Use getStorefront */ getStorefrontIOS: Promise; /** * Return the JWS string for a transaction (StoreKit 2). - * See: https://www.openiap.dev/docs/apis/ios/get-transaction-jws-ios + * See: https://openiap.dev/docs/apis/ios/get-transaction-jws-ios */ getTransactionJwsIOS?: Promise<(string | null)>; /** * Check whether the user has any active subscription. - * See: https://www.openiap.dev/docs/apis/has-active-subscriptions + * See: https://openiap.dev/docs/apis/has-active-subscriptions */ hasActiveSubscriptions: Promise; /** * Check eligibility for the custom-link variant of external purchase (iOS 18.1+). * Returns true if the app can use custom external purchase links. * Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/iseligible - * See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios + * See: https://openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios */ isEligibleForExternalPurchaseCustomLinkIOS: Promise; /** * Check intro-offer eligibility for a subscription group. - * See: https://www.openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios + * See: https://openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios */ isEligibleForIntroOfferIOS: Promise; /** * Check whether a transaction's JWS verification passed (StoreKit 2). - * See: https://www.openiap.dev/docs/apis/ios/is-transaction-verified-ios + * See: https://openiap.dev/docs/apis/ios/is-transaction-verified-ios */ isTransactionVerifiedIOS: Promise; /** * Get the latest verified transaction for a product, using StoreKit 2. - * See: https://www.openiap.dev/docs/apis/ios/latest-transaction-ios + * See: https://openiap.dev/docs/apis/ios/latest-transaction-ios */ latestTransactionIOS?: Promise<(PurchaseIOS | null)>; /** * Get subscription status objects from StoreKit 2 (iOS 15+). - * See: https://www.openiap.dev/docs/apis/ios/subscription-status-ios + * See: https://openiap.dev/docs/apis/ios/subscription-status-ios */ subscriptionStatusIOS: Promise; /** * Deprecated. Legacy App Store receipt validation — use verifyPurchase instead. - * See: https://www.openiap.dev/docs/apis/ios/validate-receipt-ios + * See: https://openiap.dev/docs/apis/ios/validate-receipt-ios * @deprecated Use verifyPurchase */ validateReceiptIOS: Promise; diff --git a/packages/kit/.env.example b/packages/kit/.env.example index 5978d5f8..2b0a9ae4 100644 --- a/packages/kit/.env.example +++ b/packages/kit/.env.example @@ -54,10 +54,10 @@ CONVEX_DEPLOY_KEY= # tune burst protection; the Convex-side monthly plan quota enforces # sustained usage separately. # ──────────────────────────────────────────────────────────────── -# Maximum burst size. Default: 60. -# RATE_LIMIT_CAPACITY=60 -# Steady-state refill, tokens per second. Default: 1 (= 60 req/min). -# RATE_LIMIT_REFILL_PER_SEC=1 +# Maximum burst size. Default: 600. +# RATE_LIMIT_CAPACITY=600 +# Steady-state refill, tokens per second. Default: 10 (= 600 req/min). +# RATE_LIMIT_REFILL_PER_SEC=10 # Upper bound on the in-memory bucket store (LRU-evicts oldest when # exceeded). Caps resident memory if a caller churns random API keys # — apiKeyMiddleware only parses the Bearer header; database validation diff --git a/packages/kit/CONVENTION.md b/packages/kit/CONVENTION.md index 784de0ad..fdcd5edb 100644 --- a/packages/kit/CONVENTION.md +++ b/packages/kit/CONVENTION.md @@ -147,7 +147,7 @@ checkouts. If you really need to bypass, fix the underlying issue rather than passing `--no-verify`. `smoke:server` (`scripts/smoke-server.sh`) compiles the Bun binary, -boots it on port 3100, and probes `/health`, `/`, `/api/v1` — catches +boots it on port 3100, and probes `/health`, `/`, `/v1`, `/api/v1` — catches startup regressions (missing env, bind conflicts, missing `dist/index.html`). diff --git a/packages/kit/README.md b/packages/kit/README.md index cb938bec..72315150 100644 --- a/packages/kit/README.md +++ b/packages/kit/README.md @@ -22,7 +22,7 @@ don't control. ```text openiap-kit/ ├── src/ # React 19 + Vite SPA (dashboard, auth, project UIs) -├── server/ # Hono + Bun server (/api/v1/* receipt validation) + static SPA serving +├── server/ # Hono + Bun server (/v1/* API + /api/v1/* compat) + static SPA serving ├── convex/ # Convex backend (auth, orgs, projects, API keys, receipts) ├── public/ # SPA static assets (favicons, og-image, etc.) ├── Dockerfile # Multi-stage build → single-binary Fly.io image @@ -38,7 +38,7 @@ One package, one binary, one Fly.io app. - **Meta Horizon entitlement verification** (Graph API) - **Scoped API keys per project** with usage telemetry - **Organization + project multi-tenancy** via Convex -- **Free for everyone** — no paywall, no usage caps. Sustained by sponsors at [openiap.dev/sponsors](https://www.openiap.dev/sponsors) +- **Free for everyone** — no paywall, no usage caps. Sustained by sponsors at [openiap.dev/sponsors](https://openiap.dev/sponsors) - **Email OTP (Resend) + GitHub OAuth** via `@convex-dev/auth` - **OpenAPI spec** auto-generated by `hono-openapi` @@ -83,7 +83,7 @@ bun run dev:server # Hono on http://localhost:3000 ```bash bun run build:all # Vite build + Bun compile → ./openiap-kit-server -./openiap-kit-server # serves /api/v1/* + static SPA on :3000 +./openiap-kit-server # serves /v1/*, /api/v1/* + static SPA on :3000 ``` ## Operations @@ -96,20 +96,27 @@ point Fly.io liveness/readiness probes at. ### Graceful shutdown The server installs `SIGTERM` and `SIGINT` handlers that call -`Bun.serve().stop()` and let in-flight `/api/v1/*` requests drain before -the process exits. Fly.io sends `SIGTERM` before stopping a machine, so -deploys don't cut off requests mid-verify. +`Bun.serve().stop()` and let in-flight `/v1/*` and `/api/v1/*` requests +drain before the process exits. Fly.io sends `SIGTERM` before stopping a +machine, so deploys don't cut off requests mid-verify. ### Rate limiting -`/api/v1/purchase/verify` is protected by an in-memory token bucket -keyed on the SHA-256 of the API key. Defaults: **60-request burst, 1 -req/sec steady state** (= 60 req/min sustained). Tunable via +`/v1/purchase/verify` is protected by an in-memory token bucket keyed +on the SHA-256 of the API key. The `/api/v1/purchase/verify` +compatibility alias uses the same middleware. Defaults: **600-request +burst, 10 req/sec steady state** (= 600 req/min sustained). Tunable via `RATE_LIMIT_CAPACITY` and `RATE_LIMIT_REFILL_PER_SEC` — see `.env.example`. Responses carry `X-RateLimit-Limit` and `X-RateLimit-Remaining`; when the bucket is empty the server returns `429 RATE_LIMITED` with a `Retry-After` header. +The verify endpoint also has a per-(API key, payload) replay guard for +the exact same receipt: **30-request burst, ~1/min sustained**, plus a +5-minute cooldown after the upstream store rejects a payload as +invalid. Those paths return `429 DUPLICATE_PAYLOAD` or +`429 REPEATED_FAILURE` with `Retry-After`. + The bucket store itself is bounded (default **10,000 entries**, `RATE_LIMIT_MAX_STORE`) with LRU eviction — an attacker churning random API keys past the parse-only `apiKeyMiddleware` can't grow the @@ -122,20 +129,28 @@ out, see the note at the top of ### Request logging -Every request to `/api/v1/purchase/verify` emits one structured JSON log -line to stdout with `{ corrId, method, path, statusCode, durationMs, -apiKeyHash, store, isValid, state }`. The hashed API key is the same -16-hex SHA-256 prefix the rate limiter uses — the plaintext key is -never logged. +Every request that passes the auth-header shape check on +`/v1/purchase/verify` or its `/api/v1/purchase/verify` compatibility +alias emits one structured JSON log line to stdout with `{ corrId, +method, path, statusCode, durationMs, apiKeyHash, store, isValid, +state }`. The hashed API key is the same 16-hex SHA-256 prefix the rate +limiter uses — the plaintext key is never logged. -Each response also carries an `X-Correlation-Id` header so customer -support tickets can cross-reference logs. +Those logged responses also carry an `X-Correlation-Id` header so +customer support tickets can cross-reference logs. Missing or malformed +Authorization headers return before the logger runs. ### Input size limits -`jws` (Apple) and `purchaseToken` (Google) are bounded at 16 KB and 2 KB -respectively. Oversized requests return `400 INVALID_INPUT` before -doing any Convex work. +Receipt-verification request bodies are capped at 32 KB before JSON +parsing. Product-management writes are capped at 64 KB, subscription +user-binding writes at 8 KB, and webhook pushes at 256 KB. `jws` +(Apple) and `purchaseToken` (Google / subscription binding) are bounded +at 16 KB and 2 KB respectively; `userId`, `sku`, and `productId` are +bounded at 256 chars where accepted. Oversized fields return +`400 INVALID_INPUT`; oversized request bodies return +`413 PAYLOAD_TOO_LARGE`. Invalid inputs stop before upstream store calls +or Convex mutations. ### Outbound retries (Google Play) @@ -196,8 +211,8 @@ checkouts. Don't bypass with `--no-verify` — fix the underlying issue. `smoke:server` compiles the Bun binary via `build:all` and runs [`scripts/smoke-server.sh`](scripts/smoke-server.sh), which boots the server on port 3100, polls `/health` until ready, and probes `/`, -`/api/v1`, `/health` — catches startup regressions (missing env, bind -conflicts, missing `dist/index.html`). Same script runs in CI. +`/v1`, `/api/v1`, `/health` — catches startup regressions (missing env, +bind conflicts, missing `dist/index.html`). Same script runs in CI. To run ad-hoc: @@ -281,7 +296,7 @@ IAPKit is one of several ways to use the OpenIAP specification: | **Native modules** (free, MIT) | openiap-apple, openiap-google | same | | **Receipt validation backend** | IAPKit SaaS — free for everyone | Deploy this repo yourself (MIT) | -The specification ([openiap.dev](https://openiap.dev)) is 100% open source. IAPKit's hosted service is free for every developer; infrastructure is sustained by community sponsorship at [openiap.dev/sponsors](https://www.openiap.dev/sponsors). You can always run this repo on your own infrastructure and pay no recurring fees. +The specification ([openiap.dev](https://openiap.dev)) is 100% open source. IAPKit's hosted service is free for every developer; infrastructure is sustained by community sponsorship at [openiap.dev/sponsors](https://openiap.dev/sponsors). You can always run this repo on your own infrastructure and pay no recurring fees. ## License diff --git a/packages/kit/convex/ResendOTP.ts b/packages/kit/convex/ResendOTP.ts index e088d2c2..a1656541 100644 --- a/packages/kit/convex/ResendOTP.ts +++ b/packages/kit/convex/ResendOTP.ts @@ -3,6 +3,21 @@ import Resend from "@auth/core/providers/resend"; import { alphabet, generateRandomString } from "oslo/crypto"; import { Resend as ResendAPI } from "resend"; +function describeErrorForLog(error: unknown): string { + if (error instanceof Error) return error.name; + if (error && typeof error === "object" && "name" in error) { + const name = (error as { name?: unknown }).name; + if (typeof name === "string") return name; + } + return typeof error; +} + +function emailDomainForLog(email: string): string { + const atIndex = email.lastIndexOf("@"); + if (atIndex < 0 || atIndex === email.length - 1) return "(invalid email)"; + return `*@${email.slice(atIndex + 1)}`; +} + const createOTPEmailTemplate = (code: string, lang: "en" | "ko" | "ja") => { const messages = { en: { @@ -234,8 +249,8 @@ const createResendOTPProvider = (locale: OTPLocale) => }); if (error) { - console.error("Resend API error:", error); - console.error("Failed to send email to:", email); + console.error("Resend API error:", describeErrorForLog(error)); + console.error("Failed to send OTP email:", emailDomainForLog(email)); throw new Error(messages.sendFailed); } }, diff --git a/packages/kit/convex/analytics/action.ts b/packages/kit/convex/analytics/action.ts index b46400a3..09064390 100644 --- a/packages/kit/convex/analytics/action.ts +++ b/packages/kit/convex/analytics/action.ts @@ -26,6 +26,10 @@ import { internalAction } from "../_generated/server"; */ const MIXPANEL_TRACK_ENDPOINT = "https://api-eu.mixpanel.com/track"; +function describeErrorForLog(error: unknown): string { + return error instanceof Error ? error.name : typeof error; +} + export const trackFirstReceiptVerified = internalAction({ args: { projectId: v.id("projects"), @@ -77,15 +81,18 @@ export const trackFirstReceiptVerified = internalAction({ body: JSON.stringify(payload), }); if (!res.ok) { - console.error( - `Mixpanel /track returned ${res.status}: ${await res.text()}`, - ); + console.error("Mixpanel /track returned non-ok status", { + status: res.status, + }); } } catch (error) { // Never let analytics failure surface to the customer. Log and // move on — we can backfill missed events from `purchases` // history if we ever need to. - console.error("Mixpanel /track request failed:", error); + console.error( + "Mixpanel /track request failed:", + describeErrorForLog(error), + ); } }, }); diff --git a/packages/kit/convex/apiKeys/helpers.ts b/packages/kit/convex/apiKeys/helpers.ts index dba5c8ee..a28d552e 100644 --- a/packages/kit/convex/apiKeys/helpers.ts +++ b/packages/kit/convex/apiKeys/helpers.ts @@ -1,8 +1,8 @@ import { Doc } from "../_generated/dataModel"; -import { QueryCtx } from "../_generated/server"; +import { MutationCtx, QueryCtx } from "../_generated/server"; export async function getApiKeyByKey( - ctx: QueryCtx, + ctx: QueryCtx | MutationCtx, key: string, ): Promise | null> { return ctx.db diff --git a/packages/kit/convex/apiKeys/internal.ts b/packages/kit/convex/apiKeys/internal.ts index 643896f0..8f6e0936 100644 --- a/packages/kit/convex/apiKeys/internal.ts +++ b/packages/kit/convex/apiKeys/internal.ts @@ -2,54 +2,46 @@ import { internalQuery, internalMutation } from "../_generated/server"; import { v } from "convex/values"; import { ConvexError } from "convex/values"; +import { getApiKeyByKey } from "./helpers"; + // Internal query to validate an API key and get the associated project export const validateApiKey = internalQuery({ args: { apiKey: v.string(), }, handler: async (ctx, args) => { - // First check if it's a legacy API key in the projects table - const projectWithLegacyKey = await ctx.db - .query("projects") - .withIndex("by_api_key", (q) => q.eq("apiKey", args.apiKey)) - .first(); + // Check the key table first so inactive rotated keys cannot fall + // through to the legacy `projects.apiKey` column. + const apiKeyRecord = await getApiKeyByKey(ctx, args.apiKey); + if (apiKeyRecord) { + if (!apiKeyRecord.isActive) { + return { isValid: false, reason: "API key is inactive" }; + } + + const project = await ctx.db.get(apiKeyRecord.projectId); + if (!project) { + return { isValid: false, reason: "Associated project not found" }; + } - if (projectWithLegacyKey) { - // Legacy key found, return project return { isValid: true, - projectId: projectWithLegacyKey._id, - organizationId: projectWithLegacyKey.organizationId, - keyId: undefined, // No keyId for legacy keys + projectId: apiKeyRecord.projectId, + organizationId: apiKeyRecord.organizationId, + keyId: apiKeyRecord._id, }; } - // Check the new apiKeys table - const apiKeyRecord = await ctx.db - .query("apiKeys") - .withIndex("by_key", (q) => q.eq("key", args.apiKey)) + const legacyProject = await ctx.db + .query("projects") + .withIndex("by_api_key", (q) => q.eq("apiKey", args.apiKey)) .first(); - - if (!apiKeyRecord) { - return { isValid: false }; - } - - // Check if the key is active - if (!apiKeyRecord.isActive) { - return { isValid: false, reason: "API key is inactive" }; - } - - // Get the project - const project = await ctx.db.get(apiKeyRecord.projectId); - if (!project) { - return { isValid: false, reason: "Associated project not found" }; - } + if (!legacyProject) return { isValid: false }; return { isValid: true, - projectId: apiKeyRecord.projectId, - organizationId: apiKeyRecord.organizationId, - keyId: apiKeyRecord._id, + projectId: legacyProject._id, + organizationId: legacyProject.organizationId, + keyId: undefined, }; }, }); diff --git a/packages/kit/convex/apiKeys/query.ts b/packages/kit/convex/apiKeys/query.ts index 005a610c..763ad716 100644 --- a/packages/kit/convex/apiKeys/query.ts +++ b/packages/kit/convex/apiKeys/query.ts @@ -1,8 +1,28 @@ import { query } from "../_generated/server"; +import type { Doc } from "../_generated/dataModel"; import { v } from "convex/values"; import { getAuthUserId } from "@convex-dev/auth/server"; import { ConvexError } from "convex/values"; +type SafeApiKey = Omit, "key"> & { + keyPreview: string; +}; + +function getApiKeyPreview(key: string): string { + const suffix = key.slice(-4); + if (key.startsWith("openiap-kit_")) { + return `openiap-kit_...${suffix}`; + } + return `...${suffix}`; +} + +function toSafeApiKey({ key, ...apiKey }: Doc<"apiKeys">): SafeApiKey { + return { + ...apiKey, + keyPreview: getApiKeyPreview(key), + }; +} + // Get all API keys for a project export const listProjectApiKeys = query({ args: { @@ -39,8 +59,8 @@ export const listProjectApiKeys = query({ .order("desc") .collect(); - // Return all keys with full data for the frontend - return apiKeys; + // Full keys are only returned by create/regenerate mutations. + return apiKeys.map(toSafeApiKey); }, }); @@ -127,9 +147,7 @@ export const getById = query({ const creator = await ctx.db.get(apiKey.createdBy); return { - ...apiKey, - // Still mask the key for security - keyPreview: `${apiKey.key.slice(0, 8)}...${apiKey.key.slice(-4)}`, + ...toSafeApiKey(apiKey), creatorName: creator?.name || "Unknown", creatorEmail: creator?.email || "Unknown", }; diff --git a/packages/kit/convex/certificates/apple_root_certificates.ts b/packages/kit/convex/certificates/apple_root_certificates.ts index 49450a48..e2e3b059 100644 --- a/packages/kit/convex/certificates/apple_root_certificates.ts +++ b/packages/kit/convex/certificates/apple_root_certificates.ts @@ -59,7 +59,10 @@ export function loadAppleRootCertificates(): Buffer[] { const buffer = Buffer.from(cert.base64, "base64"); certificates.push(buffer); } catch (error) { - console.warn(`❌ Failed to load ${cert.name}:`, error); + console.warn( + `Failed to load ${cert.name}:`, + error instanceof Error ? error.name : typeof error, + ); } } diff --git a/packages/kit/convex/files/internal.ts b/packages/kit/convex/files/internal.ts index 7e28e5b3..49eec728 100644 --- a/packages/kit/convex/files/internal.ts +++ b/packages/kit/convex/files/internal.ts @@ -8,6 +8,10 @@ import { v } from "convex/values"; import { ConvexError } from "convex/values"; import { internal } from "../_generated/api"; +function describeErrorForLog(error: unknown): string { + return error instanceof Error ? error.name : typeof error; +} + // Internal query to get file record export const getFileRecord = internalQuery({ args: { @@ -404,7 +408,10 @@ export const cleanupOldFiles = internalMutation({ await ctx.db.delete(file._id); deletedCount++; } catch (error) { - console.error(`Failed to delete file ${file._id}:`, error); + console.error("Failed to delete file", { + fileId: file._id, + error: describeErrorForLog(error), + }); } } diff --git a/packages/kit/convex/products/asc.test.ts b/packages/kit/convex/products/asc.test.ts index a22fe69e..16d986ff 100644 --- a/packages/kit/convex/products/asc.test.ts +++ b/packages/kit/convex/products/asc.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { + ascCustomerPriceToMicros, mapAscOfferDurationToIso, mapAscOfferKind, mapBillingPeriodToAsc, @@ -9,6 +10,20 @@ import { pickPricePointIdMatching, } from "./asc"; +describe("ascCustomerPriceToMicros", () => { + it("converts ASC customerPrice strings to micros", () => { + expect(ascCustomerPriceToMicros("0.99")).toBe(990_000); + expect(ascCustomerPriceToMicros("9")).toBe(9_000_000); + }); + + it("returns undefined for malformed or unsafe prices", () => { + expect(ascCustomerPriceToMicros(undefined)).toBeUndefined(); + expect(ascCustomerPriceToMicros("abc")).toBeUndefined(); + expect(ascCustomerPriceToMicros("-1")).toBeUndefined(); + expect(ascCustomerPriceToMicros("10000000000")).toBeUndefined(); + }); +}); + describe("pickPricePointIdMatching", () => { const list = { data: [ @@ -37,6 +52,11 @@ describe("pickPricePointIdMatching", () => { type: "inAppPurchasePricePoints" as const, attributes: { customerPrice: "abc" }, }, + { + id: "tier-unsafe", + type: "inAppPurchasePricePoints" as const, + attributes: { customerPrice: "9007199254.740993" }, + }, { id: "tier-empty", type: "inAppPurchasePricePoints" as const, @@ -53,6 +73,14 @@ describe("pickPricePointIdMatching", () => { expect(pickPricePointIdMatching(list, 1_500_000)).toBeNull(); }); + it("returns null for invalid requested amounts", () => { + expect(pickPricePointIdMatching(list, -1)).toBeNull(); + expect(pickPricePointIdMatching(list, 1.5)).toBeNull(); + expect( + pickPricePointIdMatching(list, Number.MAX_SAFE_INTEGER + 1), + ).toBeNull(); + }); + it("matches an exact tier on the cent boundary", () => { expect(pickPricePointIdMatching(list, 9_990_000)).toBe("tier-999"); expect(pickPricePointIdMatching(list, 290_000)).toBe("tier-29"); @@ -64,8 +92,9 @@ describe("pickPricePointIdMatching", () => { expect(pickPricePointIdMatching(list, 9_985_000)).toBe("tier-999"); }); - it("skips malformed and missing customerPrice rows", () => { + it("skips malformed, missing, and unsafe customerPrice rows", () => { expect(pickPricePointIdMatching(list, 0)).toBeNull(); + expect(pickPricePointIdMatching(list, Number.MAX_SAFE_INTEGER)).toBeNull(); }); }); diff --git a/packages/kit/convex/products/asc.ts b/packages/kit/convex/products/asc.ts index 882b30e1..21ab38d8 100644 --- a/packages/kit/convex/products/asc.ts +++ b/packages/kit/convex/products/asc.ts @@ -1,8 +1,10 @@ "use node"; import { v } from "convex/values"; +import { getAuthUserId } from "@convex-dev/auth/server"; import { action, internalAction, type ActionCtx } from "../_generated/server"; import { internal } from "../_generated/api"; +import type { Doc, Id } from "../_generated/dataModel"; import { getProjectByApiKey } from "../purchases/shared"; import { mapWithConcurrency } from "../utils/concurrency"; import { mintAscJwt } from "./jwt"; @@ -34,7 +36,7 @@ type AscCredentials = { }; async function resolveAscCredentials( ctx: ActionCtx, - project: Awaited>, + project: Doc<"projects">, options: { detailedErrors?: boolean } = {}, ): Promise { // Apple uses ONE Issuer ID per team across both API gateways @@ -133,6 +135,42 @@ async function resolveAscCredentials( return { issuerId, keyId, keyContent }; } +async function getProjectForActionArgs( + ctx: ActionCtx, + args: { apiKey?: string; projectId?: Id<"projects"> }, +): Promise> { + if (args.projectId) { + const userId: Id<"users"> | null = await getAuthUserId(ctx); + if (!userId) { + throw new Error("Not authenticated"); + } + + const project: Doc<"projects"> | null = await ctx.runQuery( + internal.projects.internal.getProjectById, + { projectId: args.projectId }, + ); + if (!project) { + throw new Error("Project not found"); + } + + const membership = await ctx.runQuery( + internal.organizations.internal.getMembership, + { userId, organizationId: project.organizationId }, + ); + if (!membership) { + throw new Error("Not a member of this organization"); + } + + return project; + } + + if (args.apiKey !== undefined) { + return await getProjectByApiKey(ctx, args.apiKey); + } + + throw new Error("apiKey or projectId is required"); +} + // App Store Connect REST client + push-sync action. // // Auth: every request carries a freshly-minted ES256 JWT signed with @@ -781,13 +819,14 @@ export function pickPricePointIdMatching( targetMicros: number, ): string | null { if (!list) return null; + if (!Number.isSafeInteger(targetMicros) || targetMicros < 0) return null; const targetCents = Math.round(targetMicros / 10_000); for (const point of list.data) { - const raw = point.attributes?.customerPrice; - if (!raw) continue; - const n = Number(raw); - if (!Number.isFinite(n)) continue; - const pointCents = Math.round(n * 100); + const pointMicros = ascCustomerPriceToMicros( + point.attributes?.customerPrice, + ); + if (pointMicros === undefined) continue; + const pointCents = Math.round(pointMicros / 10_000); if (Math.abs(pointCents - targetCents) <= 1) return point.id; } return null; @@ -925,16 +964,26 @@ function parseAssignedPrice( const pointId = row.relationships?.[relationshipKey]?.data?.id; if (!pointId) return {}; const point = resp.included?.find((entry) => entry.id === pointId); - const raw = point?.attributes?.customerPrice; - if (!raw) return {}; - const n = Number(raw); - if (!Number.isFinite(n)) return {}; + const priceAmountMicros = ascCustomerPriceToMicros( + point?.attributes?.customerPrice, + ); + if (priceAmountMicros === undefined) return {}; return { - priceAmountMicros: Math.round(n * 1_000_000), + priceAmountMicros, currency: "USD", }; } +export function ascCustomerPriceToMicros( + raw: string | undefined, +): number | undefined { + if (!raw) return undefined; + const n = Number(raw); + if (!Number.isFinite(n) || n < 0) return undefined; + const micros = Math.round(n * 1_000_000); + return Number.isSafeInteger(micros) ? micros : undefined; +} + function extractAscError(parsed: unknown): string { if ( parsed && @@ -1736,13 +1785,16 @@ async function performIosSync( // thrown Error so the dashboard can show a toast and degrade // gracefully (the field stays a free-text input). export const listSubscriptionGroupsAppleIOS = action({ - args: { apiKey: v.string() }, + args: { + apiKey: v.optional(v.string()), + projectId: v.optional(v.id("projects")), + }, returns: v.array(v.object({ id: v.string(), referenceName: v.string() })), handler: async ( ctx, args, ): Promise> => { - const project = await getProjectByApiKey(ctx, args.apiKey); + const project = await getProjectForActionArgs(ctx, args); if (!project.iosAppAppleId) { throw new Error("Project iosAppAppleId is not configured"); } @@ -1894,11 +1946,9 @@ export function parseIntroOffers( const point = pointId ? resp.included?.find((entry) => entry.id === pointId) : undefined; - const raw = point?.attributes?.customerPrice; - const n = raw ? Number(raw) : Number.NaN; - const priceAmountMicros = Number.isFinite(n) - ? Math.round(n * 1_000_000) - : undefined; + const priceAmountMicros = ascCustomerPriceToMicros( + point?.attributes?.customerPrice, + ); return { id: row.id, kind: mapAscOfferKind(row.attributes?.offerMode), diff --git a/packages/kit/convex/products/jobs.ts b/packages/kit/convex/products/jobs.ts index 37ed84ff..874cb3ae 100644 --- a/packages/kit/convex/products/jobs.ts +++ b/packages/kit/convex/products/jobs.ts @@ -7,10 +7,15 @@ import { internalQuery, mutation, query, + type MutationCtx, type QueryCtx, } from "../_generated/server"; import { internal } from "../_generated/api"; import type { Doc, Id } from "../_generated/dataModel"; +import { + resolveProjectByApiKeyFromDb, + resolveProjectByIdForCurrentUserFromDb, +} from "../projects/helpers"; import { ErrorCode, createError } from "../utils/errors"; // Per-job hard ceiling. Convex actions cap at ~10min; we allow 9min @@ -64,13 +69,11 @@ const directionValidator = v.union( // `getAuthUserId` and broke the documented HTTP API entirely // (Copilot review on PR #127). async function resolveProjectByApiKey( - ctx: QueryCtx, + ctx: QueryCtx | MutationCtx, apiKey: string, ): Promise<{ project: Doc<"projects">; userId: Id<"users"> | null }> { - const project = await ctx.db - .query("projects") - .withIndex("by_api_key", (q) => q.eq("apiKey", apiKey)) - .first(); + const resolved = await resolveProjectByApiKeyFromDb(ctx, apiKey); + const project = resolved?.project ?? null; if (!project) { throw createError(ErrorCode.PROJECT_NOT_FOUND); } @@ -98,13 +101,57 @@ async function resolveProjectByApiKey( return { project, userId }; } +async function resolveProjectForReadArgs( + ctx: QueryCtx, + args: { apiKey?: string; projectId?: Id<"projects"> }, +): Promise> { + if (args.projectId) { + const resolved = await resolveProjectByIdForCurrentUserFromDb( + ctx, + args.projectId, + ); + if (!resolved) { + throw createError(ErrorCode.PROJECT_NOT_FOUND); + } + return resolved.project; + } + + if (args.apiKey !== undefined) { + return (await resolveProjectByApiKey(ctx, args.apiKey)).project; + } + + throw createError(ErrorCode.INVALID_INPUT, "apiKey or projectId is required"); +} + +async function resolveProjectForMutationArgs( + ctx: MutationCtx, + args: { apiKey?: string; projectId?: Id<"projects"> }, +): Promise<{ project: Doc<"projects">; userId: Id<"users"> | null }> { + if (args.projectId) { + const resolved = await resolveProjectByIdForCurrentUserFromDb( + ctx, + args.projectId, + ); + if (!resolved) { + throw createError(ErrorCode.PROJECT_NOT_FOUND); + } + return resolved; + } + + if (args.apiKey !== undefined) { + return resolveProjectByApiKey(ctx, args.apiKey); + } + + throw createError(ErrorCode.INVALID_INPUT, "apiKey or projectId is required"); +} + // Authenticate `(apiKey, jobId)` together: resolve the project from // the apiKey, then verify the job belongs to that project. This // ensures the apiKey acts as a per-project capability — a stolen // jobId from one project can't be cancelled / read by another // project's apiKey. async function resolveJobByApiKey( - ctx: QueryCtx, + ctx: QueryCtx | MutationCtx, apiKey: string, jobId: Id<"productSyncJobs">, ): Promise<{ job: Doc<"productSyncJobs">; project: Doc<"projects"> }> { @@ -121,15 +168,32 @@ async function resolveJobByApiKey( return { job, project }; } +async function resolveJobForMutationArgs( + ctx: MutationCtx, + args: { + apiKey?: string; + projectId?: Id<"projects">; + jobId: Id<"productSyncJobs">; + }, +): Promise<{ job: Doc<"productSyncJobs">; project: Doc<"projects"> }> { + const { project } = await resolveProjectForMutationArgs(ctx, args); + const job = await ctx.db.get(args.jobId); + if (!job || job.projectId !== project._id) { + throw createError(ErrorCode.INVALID_INPUT, "Sync job not found"); + } + return { job, project }; +} + // Latest job (any status) for a project+platform — drives the // dashboard's button state, progress, and last-result toast. export const getActiveSyncJob = query({ args: { - apiKey: v.string(), + apiKey: v.optional(v.string()), + projectId: v.optional(v.id("projects")), platform: platformValidator, }, handler: async (ctx, args) => { - const { project } = await resolveProjectByApiKey(ctx, args.apiKey); + const project = await resolveProjectForReadArgs(ctx, args); // Composite index `by_project_platform_created` narrows the // index range to just this (project, platform) — replaces the // earlier `by_project_and_created` + in-memory `.filter()` @@ -161,7 +225,8 @@ export const getSyncJobById = query({ // page reload doesn't fan out duplicate workers. export const enqueueProductSync = mutation({ args: { - apiKey: v.string(), + apiKey: v.optional(v.string()), + projectId: v.optional(v.id("projects")), platform: platformValidator, direction: v.optional(directionValidator), dryRun: v.optional(v.boolean()), @@ -171,7 +236,7 @@ export const enqueueProductSync = mutation({ deduped: v.boolean(), }), handler: async (ctx, args) => { - const { project, userId } = await resolveProjectByApiKey(ctx, args.apiKey); + const { project, userId } = await resolveProjectForMutationArgs(ctx, args); // Atomic dedup via the project's `activeSyncJobIds` lock field. // Reading and writing the project doc lets Convex's OCC collapse // two concurrent enqueue mutations onto the same job: both read @@ -314,11 +379,12 @@ export const runProductSyncPurgeLocal = internalAction({ // that's enough to stop a runaway sync within seconds on most paths. export const cancelProductSync = mutation({ args: { - apiKey: v.string(), + apiKey: v.optional(v.string()), + projectId: v.optional(v.id("projects")), jobId: v.id("productSyncJobs"), }, handler: async (ctx, args) => { - const { job } = await resolveJobByApiKey(ctx, args.apiKey, args.jobId); + const { job } = await resolveJobForMutationArgs(ctx, args); if (job.status !== "queued" && job.status !== "running") { return { ok: false, reason: "not active" as const }; } @@ -336,11 +402,12 @@ export const cancelProductSync = mutation({ // semantics stay distinct from operator dismiss. export const dismissCompletedJob = mutation({ args: { - apiKey: v.string(), + apiKey: v.optional(v.string()), + projectId: v.optional(v.id("projects")), jobId: v.id("productSyncJobs"), }, handler: async (ctx, args) => { - const { job } = await resolveJobByApiKey(ctx, args.apiKey, args.jobId); + const { job } = await resolveJobForMutationArgs(ctx, args); if (job.status !== "succeeded" && job.status !== "failed") { return { ok: false as const }; } diff --git a/packages/kit/convex/products/mutation.ts b/packages/kit/convex/products/mutation.ts index 4f78835d..eaaccf7c 100644 --- a/packages/kit/convex/products/mutation.ts +++ b/packages/kit/convex/products/mutation.ts @@ -1,6 +1,11 @@ -import { mutation } from "../_generated/server"; +import { mutation, type MutationCtx } from "../_generated/server"; import { v } from "convex/values"; -import type { Doc } from "../_generated/dataModel"; +import type { Doc, Id } from "../_generated/dataModel"; + +import { + resolveProjectByApiKeyFromDb, + resolveProjectByIdForCurrentUserFromDb, +} from "../projects/helpers"; const platformValidator = v.union(v.literal("IOS"), v.literal("Android")); const typeValidator = v.union( @@ -15,6 +20,26 @@ const stateValidator = v.union( v.literal("Removed"), ); +async function resolveProjectForMutationArgs( + ctx: MutationCtx, + args: { apiKey?: string; projectId?: Id<"projects"> }, +): Promise | null> { + if (args.projectId) { + const resolved = await resolveProjectByIdForCurrentUserFromDb( + ctx, + args.projectId, + ); + return resolved?.project ?? null; + } + + if (args.apiKey !== undefined) { + const resolved = await resolveProjectByApiKeyFromDb(ctx, args.apiKey); + return resolved?.project ?? null; + } + + throw new Error("apiKey or projectId is required"); +} + // Public mutation: upsert a product in kit's catalog. Authoritative // state lives in App Store Connect / Play Console; this row is a // kit-side cache so the dashboard, MCP server, and SDKs share one @@ -22,7 +47,8 @@ const stateValidator = v.union( // — until then, treat this as a hand-managed catalog. export const upsertProduct = mutation({ args: { - apiKey: v.string(), + apiKey: v.optional(v.string()), + projectId: v.optional(v.id("projects")), productId: v.string(), platform: platformValidator, type: typeValidator, @@ -50,20 +76,19 @@ export const upsertProduct = mutation({ created: v.boolean(), }), handler: async (ctx, args) => { - const project = await ctx.db - .query("projects") - .withIndex("by_api_key", (q) => q.eq("apiKey", args.apiKey)) - .unique(); + const project = await resolveProjectForMutationArgs(ctx, args); if (!project) throw new Error("Invalid API key"); - // Reject negative prices. The catalog row would otherwise round- - // trip into push-sync (asc.ts / play.ts) and either crash on - // Apple's price-tier lookup or land a negative `priceMicros` on - // Play, neither of which the operator can correct from the - // dashboard later (PR #124 - // (https://github.com/hyodotdev/openiap/pull/124) review). - if (args.priceAmountMicros !== undefined && args.priceAmountMicros < 0) { - throw new Error("priceAmountMicros must be non-negative"); + // Reject unsafe prices. The catalog row round-trips through JS + // numbers into push-sync (asc.ts / play.ts); values beyond the + // safe-integer range can already be rounded before they reach the + // store API. + if ( + args.priceAmountMicros !== undefined && + (!Number.isSafeInteger(args.priceAmountMicros) || + args.priceAmountMicros < 0) + ) { + throw new Error("priceAmountMicros must be a non-negative safe integer"); } // iOS subscriptions REQUIRE a subscriptionGroupName upstream — @@ -160,7 +185,8 @@ export const upsertProduct = mutation({ // drive-by clobber. export const setProductState = mutation({ args: { - apiKey: v.string(), + apiKey: v.optional(v.string()), + projectId: v.optional(v.id("projects")), productId: v.string(), platform: platformValidator, state: stateValidator, @@ -170,10 +196,7 @@ export const setProductState = mutation({ state: stateValidator, }), handler: async (ctx, args) => { - const project = await ctx.db - .query("projects") - .withIndex("by_api_key", (q) => q.eq("apiKey", args.apiKey)) - .unique(); + const project = await resolveProjectForMutationArgs(ctx, args); if (!project) throw new Error("Invalid API key"); const existing = await ctx.db @@ -197,16 +220,14 @@ export const setProductState = mutation({ export const removeProduct = mutation({ args: { - apiKey: v.string(), + apiKey: v.optional(v.string()), + projectId: v.optional(v.id("projects")), productId: v.string(), platform: platformValidator, }, returns: v.object({ ok: v.boolean() }), handler: async (ctx, args) => { - const project = await ctx.db - .query("projects") - .withIndex("by_api_key", (q) => q.eq("apiKey", args.apiKey)) - .unique(); + const project = await resolveProjectForMutationArgs(ctx, args); if (!project) return { ok: false }; const existing = await ctx.db diff --git a/packages/kit/convex/products/play.test.ts b/packages/kit/convex/products/play.test.ts index 1041319e..8bef4454 100644 --- a/packages/kit/convex/products/play.test.ts +++ b/packages/kit/convex/products/play.test.ts @@ -1,6 +1,32 @@ import { describe, expect, it } from "vitest"; -import { basePlanIdForPeriod, moneyToMicros } from "./play"; +import { + basePlanIdForPeriod, + moneyToMicros, + playPriceMicrosToNumber, +} from "./play"; + +describe("playPriceMicrosToNumber", () => { + it("accepts non-negative safe integer price strings", () => { + expect(playPriceMicrosToNumber("0")).toBe(0); + expect(playPriceMicrosToNumber("990000")).toBe(990_000); + expect(playPriceMicrosToNumber(String(Number.MAX_SAFE_INTEGER))).toBe( + Number.MAX_SAFE_INTEGER, + ); + }); + + it("rejects malformed, negative, fractional, and unsafe price strings", () => { + expect(playPriceMicrosToNumber(undefined)).toBeUndefined(); + expect(playPriceMicrosToNumber("abc")).toBeUndefined(); + expect(playPriceMicrosToNumber(" 990000 ")).toBeUndefined(); + expect(playPriceMicrosToNumber("1e6")).toBeUndefined(); + expect(playPriceMicrosToNumber("-1")).toBeUndefined(); + expect(playPriceMicrosToNumber("1.5")).toBeUndefined(); + expect( + playPriceMicrosToNumber(String(Number.MAX_SAFE_INTEGER + 1)), + ).toBeUndefined(); + }); +}); describe("moneyToMicros", () => { it("returns undefined when input is missing or has no units", () => { @@ -54,10 +80,40 @@ describe("moneyToMicros", () => { ).toBeUndefined(); }); - it("returns undefined when units is not a parseable BigInt string", () => { + it("returns undefined when units is not a non-negative decimal string", () => { expect( moneyToMicros({ currencyCode: "USD", units: "abc", nanos: 0 }), ).toBeUndefined(); + expect( + moneyToMicros({ currencyCode: "USD", units: "+1", nanos: 0 }), + ).toBeUndefined(); + expect( + moneyToMicros({ currencyCode: "USD", units: " 1", nanos: 0 }), + ).toBeUndefined(); + expect( + moneyToMicros({ currencyCode: "USD", units: "1 ", nanos: 0 }), + ).toBeUndefined(); + }); + + it("returns undefined for negative prices", () => { + expect( + moneyToMicros({ currencyCode: "USD", units: "-1", nanos: 0 }), + ).toBeUndefined(); + expect( + moneyToMicros({ currencyCode: "USD", units: "0", nanos: -1_000 }), + ).toBeUndefined(); + }); + + it("returns undefined when nanos is outside Google Money bounds", () => { + expect( + moneyToMicros({ currencyCode: "USD", units: "0", nanos: 1_000_000_000 }), + ).toBeUndefined(); + expect( + moneyToMicros({ currencyCode: "USD", units: "0", nanos: -1_000_000_000 }), + ).toBeUndefined(); + expect( + moneyToMicros({ currencyCode: "USD", units: "0", nanos: 1.5 }), + ).toBeUndefined(); }); }); diff --git a/packages/kit/convex/products/play.ts b/packages/kit/convex/products/play.ts index 6f61f117..3ffb22c7 100644 --- a/packages/kit/convex/products/play.ts +++ b/packages/kit/convex/products/play.ts @@ -899,13 +899,19 @@ function pickPlayDescription( return product.listings?.[def]?.description ?? undefined; } -function parsePlayPriceMicros( - product: androidpublisher_v3.Schema$InAppProduct, +export function playPriceMicrosToNumber( + raw: string | undefined | null, ): number | undefined { - const raw = product.defaultPrice?.priceMicros; if (!raw) return undefined; + if (!/^\d+$/.test(raw)) return undefined; const n = Number(raw); - return Number.isFinite(n) ? n : undefined; + return Number.isSafeInteger(n) && n >= 0 ? n : undefined; +} + +function parsePlayPriceMicros( + product: androidpublisher_v3.Schema$InAppProduct, +): number | undefined { + return playPriceMicrosToNumber(product.defaultPrice?.priceMicros); } function pickPlayCurrency( @@ -1092,9 +1098,10 @@ function collectPlaySubscriptionOffers( * stores price points internally — Play uses micros as the canonical * unit, so any rounding here would re-introduce drift we just cleaned * up. Resolves to `undefined` when the input has no `units`, when the - * BigInt parse throws (malformed `units` string), or when the resulting - * micros exceed `Number.MAX_SAFE_INTEGER` (≈ USD 9 billion — kit treats - * those rows as price-unknown rather than silently corrupting them). + * `units` is not a non-negative decimal string, when `nanos` falls outside + * Google Money's int32 sub-unit range, or when the resulting micros exceed + * `Number.MAX_SAFE_INTEGER` (≈ USD 9 billion — kit treats those rows as + * price-unknown rather than silently corrupting them). * * PR #124 (https://github.com/hyodotdev/openiap/pull/124) review fix. */ @@ -1103,8 +1110,11 @@ export function moneyToMicros( ): number | undefined { if (!money?.units) return undefined; try { - const microsBigInt = - BigInt(money.units) * 1_000_000n + BigInt(money.nanos ?? 0) / 1_000n; + const unitsMicros = moneyUnitsToMicros(money.units); + if (unitsMicros === undefined) return undefined; + const nanosMicros = moneyNanosToMicros(money.nanos); + if (nanosMicros === undefined) return undefined; + const microsBigInt = unitsMicros + nanosMicros; // Drop values that exceed Number.MAX_SAFE_INTEGER. The schema // stores `priceAmountMicros` as a JS `number` (IEEE 754 double), // so anything above 2^53 - 1 would silently lose precision on @@ -1113,10 +1123,7 @@ export function moneyToMicros( // very high unit values like IDR / KRW it's worth the explicit // guard rather than a silent corruption — kit treats the row as // "price unknown" and the dashboard surfaces that affordance. - if ( - microsBigInt > BigInt(Number.MAX_SAFE_INTEGER) || - microsBigInt < BigInt(Number.MIN_SAFE_INTEGER) - ) { + if (microsBigInt > BigInt(Number.MAX_SAFE_INTEGER) || microsBigInt < 0n) { return undefined; } return Number(microsBigInt); @@ -1125,6 +1132,21 @@ export function moneyToMicros( } } +function moneyUnitsToMicros(units: string): bigint | undefined { + if (!/^\d+$/.test(units)) return undefined; + return BigInt(units) * 1_000_000n; +} + +function moneyNanosToMicros( + nanos: number | null | undefined, +): bigint | undefined { + if (nanos == null) return 0n; + if (!Number.isInteger(nanos) || nanos < -999_999_999 || nanos > 999_999_999) { + return undefined; + } + return BigInt(nanos) / 1_000n; +} + /** * Map an ISO 8601 billing-period string (`P1W` / `P1M` / `P1Y` / etc.) * to a stable, descriptive basePlanId for the Play console. Play's diff --git a/packages/kit/convex/products/query.ts b/packages/kit/convex/products/query.ts index 6acf213d..2fd4949e 100644 --- a/packages/kit/convex/products/query.ts +++ b/packages/kit/convex/products/query.ts @@ -2,6 +2,11 @@ import { query } from "../_generated/server"; import { v } from "convex/values"; import type { Doc } from "../_generated/dataModel"; +import { + resolveProjectByApiKeyFromDb, + resolveProjectByIdForCurrentUserFromDb, +} from "../projects/helpers"; + const offerShape = v.object({ id: v.string(), kind: v.union( @@ -84,15 +89,18 @@ function shape(product: Doc<"products">) { export const listProducts = query({ args: { - apiKey: v.string(), + apiKey: v.optional(v.string()), + projectId: v.optional(v.id("projects")), platform: v.optional(v.union(v.literal("IOS"), v.literal("Android"))), }, returns: v.array(productShape), handler: async (ctx, args) => { - const project = await ctx.db - .query("projects") - .withIndex("by_api_key", (q) => q.eq("apiKey", args.apiKey)) - .unique(); + const resolved = args.projectId + ? await resolveProjectByIdForCurrentUserFromDb(ctx, args.projectId) + : args.apiKey + ? await resolveProjectByApiKeyFromDb(ctx, args.apiKey) + : null; + const project = resolved?.project ?? null; if (!project) return []; if (args.platform) { diff --git a/packages/kit/convex/products/sync.test.ts b/packages/kit/convex/products/sync.test.ts new file mode 100644 index 00000000..0719d5e5 --- /dev/null +++ b/packages/kit/convex/products/sync.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; + +import { isSafePriceAmountMicros } from "./sync"; + +describe("isSafePriceAmountMicros", () => { + it("accepts missing and non-negative safe integer prices", () => { + expect(isSafePriceAmountMicros(undefined)).toBe(true); + expect(isSafePriceAmountMicros(0)).toBe(true); + expect(isSafePriceAmountMicros(Number.MAX_SAFE_INTEGER)).toBe(true); + }); + + it("rejects negative, fractional, and unsafe prices", () => { + expect(isSafePriceAmountMicros(-1)).toBe(false); + expect(isSafePriceAmountMicros(1.5)).toBe(false); + expect(isSafePriceAmountMicros(Number.MAX_SAFE_INTEGER + 1)).toBe(false); + }); +}); diff --git a/packages/kit/convex/products/sync.ts b/packages/kit/convex/products/sync.ts index 57ea991a..ef819028 100644 --- a/packages/kit/convex/products/sync.ts +++ b/packages/kit/convex/products/sync.ts @@ -62,6 +62,19 @@ export function coerceBillingPeriod( : undefined; } +export function isSafePriceAmountMicros(value: number | undefined): boolean { + return value === undefined || (Number.isSafeInteger(value) && value >= 0); +} + +function assertSafePriceAmountMicros( + value: number | undefined, + fieldName: string, +): void { + if (!isSafePriceAmountMicros(value)) { + throw new Error(`${fieldName} must be a non-negative safe integer`); + } +} + // Internal mutation called by the ASC / Play push-sync actions when a // row is mirrored from the upstream store. Distinct from the public // `upsertProduct` mutation in mutation.ts so server-driven sync can't @@ -102,6 +115,14 @@ export const upsertFromStore = internalMutation({ }, returns: v.id("products"), handler: async (ctx, args) => { + assertSafePriceAmountMicros(args.priceAmountMicros, "priceAmountMicros"); + args.offers?.forEach((offer, index) => { + assertSafePriceAmountMicros( + offer.priceAmountMicros, + `offers[${index}].priceAmountMicros`, + ); + }); + // Match by (projectId, platform, productId) — apps commonly use // the same productId on both stores, and the older // (projectId, productId)-only lookup would collide and silently diff --git a/packages/kit/convex/projects/helpers.ts b/packages/kit/convex/projects/helpers.ts index 59ee57b7..02d2a792 100644 --- a/packages/kit/convex/projects/helpers.ts +++ b/packages/kit/convex/projects/helpers.ts @@ -1,7 +1,67 @@ -import type { Id } from "../_generated/dataModel"; +import type { Doc, Id } from "../_generated/dataModel"; import type { MutationCtx, QueryCtx } from "../_generated/server"; +import { getAuthUserId } from "@convex-dev/auth/server"; +import { getApiKeyByKey } from "../apiKeys/helpers"; import { deletePurchaseStatsForProject } from "../purchases/stats"; +export type ApiKeyProjectResolution = { + project: Doc<"projects">; + keyId?: Id<"apiKeys">; + organizationId: Id<"organizations">; +}; + +export async function resolveProjectByApiKeyFromDb( + ctx: QueryCtx | MutationCtx, + apiKey: string, +): Promise { + const keyRow = await getApiKeyByKey(ctx, apiKey); + if (keyRow !== null) { + if (keyRow.isActive === false) return null; + const project = await ctx.db.get(keyRow.projectId); + if (!project) return null; + return { + project, + keyId: keyRow._id, + organizationId: keyRow.organizationId, + }; + } + + const legacyProject = await ctx.db + .query("projects") + .withIndex("by_api_key", (q) => q.eq("apiKey", apiKey)) + .first(); + + if (!legacyProject) return null; + return { + project: legacyProject, + organizationId: legacyProject.organizationId, + }; +} + +export async function resolveProjectByIdForCurrentUserFromDb( + ctx: QueryCtx | MutationCtx, + projectId: Id<"projects">, +): Promise<{ + project: Doc<"projects">; + userId: Id<"users">; + role: "owner" | "admin" | "member"; +} | null> { + const userId = await getAuthUserId(ctx); + if (!userId) return null; + + const project = await ctx.db.get(projectId); + if (!project) return null; + + const membership = await ctx.db + .query("organizationMembers") + .withIndex("by_org_and_user", (q) => + q.eq("organizationId", project.organizationId).eq("userId", userId), + ) + .first(); + + return membership ? { project, userId, role: membership.role } : null; +} + /** * Delete a project and all of its Convex data (API keys, receipts, files). * Keeps the cascade logic in one place so both direct and indirect callers stay in sync. diff --git a/packages/kit/convex/projects/internal.ts b/packages/kit/convex/projects/internal.ts index d8a02e22..f72ad969 100644 --- a/packages/kit/convex/projects/internal.ts +++ b/packages/kit/convex/projects/internal.ts @@ -2,37 +2,15 @@ import { v } from "convex/values"; import { internalQuery } from "../_generated/server"; import { Doc } from "../_generated/dataModel"; -import { getApiKeyByKey } from "../apiKeys/helpers"; -import { getProjectById as getProjectByIdFromDb } from "./helpers"; +import { resolveProjectByApiKeyFromDb } from "./helpers"; export const getProjectByApiKey = internalQuery({ args: { apiKey: v.string(), }, handler: async (ctx, args): Promise | null> => { - // Preferred path: look the key up in the `apiKeys` table. This is what - // `createProject` writes every new project's default key to, and is - // what carries per-key `isActive` / rotation semantics. - const apiKey = await getApiKeyByKey(ctx, args.apiKey); - - if (apiKey !== null) { - if (apiKey.isActive === false) { - return null; - } - return getProjectByIdFromDb(ctx, apiKey.projectId); - } - - // Legacy fallback: early projects — or anything created before the - // `apiKeys` table was introduced — only had the key on the `projects` - // row. Match those via the `by_api_key` index on `projects` so - // receipt verification doesn't fail on un-migrated rows. Paid for - // only on a miss in the `apiKeys` table, so new installs pay zero. - const legacyProject = await ctx.db - .query("projects") - .withIndex("by_api_key", (q) => q.eq("apiKey", args.apiKey)) - .first(); - - return legacyProject ?? null; + const resolved = await resolveProjectByApiKeyFromDb(ctx, args.apiKey); + return resolved?.project ?? null; }, }); diff --git a/packages/kit/convex/projects/mutation.test.ts b/packages/kit/convex/projects/mutation.test.ts new file mode 100644 index 00000000..824b5a8e --- /dev/null +++ b/packages/kit/convex/projects/mutation.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; + +import { normalizeAppAppleId } from "./mutation"; + +describe("normalizeAppAppleId", () => { + it("accepts positive safe integers", () => { + expect(normalizeAppAppleId(1234567890)).toBe(1_234_567_890); + }); + + it("rejects fractional, unsafe, and non-positive values", () => { + expect(() => normalizeAppAppleId(123.45)).toThrow( + "App Apple ID must be a positive safe integer.", + ); + expect(() => normalizeAppAppleId(Number.MAX_SAFE_INTEGER + 1)).toThrow( + "App Apple ID must be a positive safe integer.", + ); + expect(() => normalizeAppAppleId(0)).toThrow( + "App Apple ID must be a positive safe integer.", + ); + }); +}); diff --git a/packages/kit/convex/projects/mutation.ts b/packages/kit/convex/projects/mutation.ts index 70e7a0c5..f370b19a 100644 --- a/packages/kit/convex/projects/mutation.ts +++ b/packages/kit/convex/projects/mutation.ts @@ -63,15 +63,15 @@ function normalizeIosBundleId(input: string): string { return normalized; } -function normalizeAppAppleId(input: number): number { - if (!Number.isFinite(input) || input <= 0) { +export function normalizeAppAppleId(input: number): number { + if (!Number.isSafeInteger(input) || input <= 0) { throw createError( ErrorCode.INVALID_INPUT, - "App Apple ID must be a positive number.", + "App Apple ID must be a positive safe integer.", ); } - return Math.trunc(input); + return input; } function normalizeAppStoreIssuerId(input: string): string { @@ -275,7 +275,7 @@ export const updateProject = mutation({ // compatible. Validation only runs when horizonEnabled === true. // horizonAppSecret intentionally accepts only a fresh value from // the client: the UI never prefills the existing secret (the query - // redacts it), so an undefined value here means "leave existing + // omits it), so an undefined value here means "leave existing // untouched". A `null` from the Horizon-off branch clears it. horizonEnabled: v.optional(v.boolean()), horizonAppId: v.optional(v.string()), diff --git a/packages/kit/convex/projects/query.ts b/packages/kit/convex/projects/query.ts index d50c1adc..81df17d4 100644 --- a/packages/kit/convex/projects/query.ts +++ b/packages/kit/convex/projects/query.ts @@ -1,23 +1,14 @@ -import { query } from "../_generated/server"; +import { query, type QueryCtx } from "../_generated/server"; import { v } from "convex/values"; import { getAuthUserId } from "@convex-dev/auth/server"; import { Doc } from "../_generated/dataModel"; -/** - * Strip long-lived server-side secrets from a project document before - * returning it to the client. The Horizon App Secret is used by the - * IAPKit server to compose `OC|APP_ID|APP_SECRET` for Meta's Graph - * API; reading it back into the dashboard would let any org member - * exfiltrate it through the browser. Apple `.p8` and Google service - * accounts live in the `files` table and were already safe — Horizon - * was the only credential stored inline on `projects`, so only it - * needs redaction here. - * - * The client still needs to know whether a secret is configured so - * the UI can show "Replace" vs. "Enter secret"; expose that as a - * derived boolean instead. - */ -function redactProjectSecrets(project: Doc<"projects">): Omit< +import { + resolveProjectByApiKeyFromDb, + resolveProjectByIdForCurrentUserFromDb, +} from "./helpers"; + +function projectWithSecretState(project: Doc<"projects">): Omit< Doc<"projects">, "horizonAppSecret" > & { @@ -31,6 +22,46 @@ function redactProjectSecrets(project: Doc<"projects">): Omit< }; } +function projectForApiKeyLookup(project: Doc<"projects">): Omit< + Doc<"projects">, + "apiKey" | "horizonAppSecret" +> & { + hasHorizonAppSecret: boolean; +} { + const { apiKey, ...rest } = projectWithSecretState(project); + void apiKey; + return rest; +} + +function projectForDashboard(project: Doc<"projects">): Omit< + Doc<"projects">, + "apiKey" | "horizonAppSecret" +> & { + hasHorizonAppSecret: boolean; +} { + return projectForApiKeyLookup(project); +} + +function projectForList( + project: Doc<"projects">, + projectIdsWithAnyKey: Set, + projectIdsWithActiveKey: Set, +): Omit, "apiKey" | "horizonAppSecret"> & { + hasApiKey: boolean; + hasHorizonAppSecret: boolean; +} { + const { apiKey, ...rest } = projectWithSecretState(project); + return { + ...rest, + // Legacy-only projects predate the apiKeys table, so fall back to + // projects.apiKey only when no apiKeys rows exist yet. Once a project has + // entered the key table, active/revoked state there is authoritative. + hasApiKey: + projectIdsWithActiveKey.has(project._id) || + (!projectIdsWithAnyKey.has(project._id) && apiKey.length > 0), + }; +} + export const listOrganizationProjects = query({ args: { organizationId: v.id("organizations") }, handler: async (ctx, args) => { @@ -59,7 +90,24 @@ export const listOrganizationProjects = query({ ) .collect(); - return projects.map(redactProjectSecrets); + const apiKeys = await ctx.db + .query("apiKeys") + .withIndex("by_organization", (q) => + q.eq("organizationId", args.organizationId), + ) + .collect(); + const projectIdsWithAnyKey = new Set( + apiKeys.map((apiKey) => apiKey.projectId), + ); + const projectIdsWithActiveKey = new Set( + apiKeys + .filter((apiKey) => apiKey.isActive) + .map((apiKey) => apiKey.projectId), + ); + + return projects.map((project) => + projectForList(project, projectIdsWithAnyKey, projectIdsWithActiveKey), + ); }, }); @@ -96,7 +144,7 @@ export const getProject = query({ ) .first(); - return project ? redactProjectSecrets(project) : null; + return project ? projectForDashboard(project) : null; }, }); @@ -128,7 +176,62 @@ export const getProjectById = query({ return null; } - return redactProjectSecrets(project); + return projectForDashboard(project); + }, +}); + +async function getWebhookApiKey(ctx: QueryCtx, project: Doc<"projects">) { + const apiKeys = await ctx.db + .query("apiKeys") + .withIndex("by_project", (q) => q.eq("projectId", project._id)) + .collect(); + const activeApiKey = apiKeys + .filter((apiKey) => apiKey.isActive) + .sort((a, b) => b.createdAt - a.createdAt)[0]; + + if (activeApiKey) { + return activeApiKey.key; + } + + // Legacy-only projects predate the apiKeys table. Once the table has rows + // for a project, do not fall back to projects.apiKey because revoked keys + // are authoritative there. + return apiKeys.length === 0 && project.apiKey.length > 0 + ? project.apiKey + : null; +} + +function webhookPathsForApiKey(apiKey: string) { + const encodedApiKey = encodeURIComponent(apiKey); + return { + unified: `/v1/webhooks/${encodedApiKey}`, + apple: `/v1/webhooks/apple/${encodedApiKey}`, + google: `/v1/webhooks/google/${encodedApiKey}`, + stream: `/v1/webhooks/stream/${encodedApiKey}`, + }; +} + +export const getWebhookEndpointPaths = query({ + args: { projectId: v.id("projects") }, + returns: v.union( + v.null(), + v.object({ + unified: v.string(), + apple: v.string(), + google: v.string(), + stream: v.string(), + }), + ), + handler: async (ctx, args) => { + const resolved = await resolveProjectByIdForCurrentUserFromDb( + ctx, + args.projectId, + ); + if (!resolved) return null; + if (resolved.role === "member") return null; + + const apiKey = await getWebhookApiKey(ctx, resolved.project); + return apiKey ? webhookPathsForApiKey(apiKey) : null; }, }); @@ -176,14 +279,13 @@ export const hasProjects = query({ }); // Public query to find project by API key (used by API verification endpoints). -// Uses the `by_api_key` index on `projects` so this is a single point lookup -// instead of a table scan. +// New keys resolve through `apiKeys` first so rotation / revocation semantics +// match the rest of the v1 surface; legacy project keys still fall back. Do +// Do not echo the apiKey back to callers; route code only needs a truthy project. export const getProjectByApiKey = query({ args: { apiKey: v.string() }, handler: async (ctx, args) => { - return await ctx.db - .query("projects") - .withIndex("by_api_key", (q) => q.eq("apiKey", args.apiKey)) - .first(); + const resolved = await resolveProjectByApiKeyFromDb(ctx, args.apiKey); + return resolved ? projectForApiKeyLookup(resolved.project) : null; }, }); diff --git a/packages/kit/convex/projects/setupStatus.ts b/packages/kit/convex/projects/setupStatus.ts index 08a663ab..6334f817 100644 --- a/packages/kit/convex/projects/setupStatus.ts +++ b/packages/kit/convex/projects/setupStatus.ts @@ -1,6 +1,11 @@ import { query } from "../_generated/server"; import { v } from "convex/values"; +import { + resolveProjectByApiKeyFromDb, + resolveProjectByIdForCurrentUserFromDb, +} from "./helpers"; + // Public query — surfaces which platforms a project has configured so // the dashboard, the SDK, and the MCP server can return a precise // "X not configured" error instead of a silent empty response. @@ -15,7 +20,10 @@ const platformShape = v.object({ }); export const getSetupStatus = query({ - args: { apiKey: v.string() }, + args: { + apiKey: v.optional(v.string()), + projectId: v.optional(v.id("projects")), + }, returns: v.object({ found: v.boolean(), projectId: v.union(v.id("projects"), v.null()), @@ -26,10 +34,12 @@ export const getSetupStatus = query({ googleServiceAccountUploaded: v.boolean(), }), handler: async (ctx, args) => { - const project = await ctx.db - .query("projects") - .withIndex("by_api_key", (q) => q.eq("apiKey", args.apiKey)) - .unique(); + const resolved = args.projectId + ? await resolveProjectByIdForCurrentUserFromDb(ctx, args.projectId) + : args.apiKey + ? await resolveProjectByApiKeyFromDb(ctx, args.apiKey) + : null; + const project = resolved?.project ?? null; if (!project) { const empty = { configured: false, missing: ["project not found"] }; diff --git a/packages/kit/convex/purchases/android.test.ts b/packages/kit/convex/purchases/android.test.ts index 7b7c9076..e2936f06 100644 --- a/packages/kit/convex/purchases/android.test.ts +++ b/packages/kit/convex/purchases/android.test.ts @@ -3,12 +3,36 @@ import { isProductNotFoundError, mapProductResponseToReceiptData, mapSubscriptionResponseToReceiptData, + parseTimeToMillis, } from "./android"; import { HarmonizedPurchaseState } from "./purchaseState"; import { mapToGooglePlayReceiptResponse } from "./shared"; const packageName = "com.example.app"; +describe("parseTimeToMillis", () => { + it("accepts decimal epoch millis and RFC3339 timestamps", () => { + expect(parseTimeToMillis("1700000000000")).toBe(1_700_000_000_000); + expect(parseTimeToMillis(" 1700000000000 ")).toBe(1_700_000_000_000); + expect(parseTimeToMillis("2025-10-13T20:13:42.748Z")).toBe( + Date.parse("2025-10-13T20:13:42.748Z"), + ); + }); + + it("rejects malformed, numeric-like, and unsafe timestamps", () => { + expect(parseTimeToMillis(undefined)).toBeUndefined(); + expect(parseTimeToMillis("")).toBeUndefined(); + expect(parseTimeToMillis("0x10")).toBeUndefined(); + expect(parseTimeToMillis("+1000")).toBeUndefined(); + expect(parseTimeToMillis("1e3")).toBeUndefined(); + expect(parseTimeToMillis("123.45")).toBeUndefined(); + expect( + parseTimeToMillis(String(Number.MAX_SAFE_INTEGER + 1)), + ).toBeUndefined(); + expect(parseTimeToMillis("not-a-date")).toBeUndefined(); + }); +}); + describe("Google Play v2 mappings", () => { it("maps productsv2.getproductpurchasev2 PURCHASED + acknowledged + not consumed to ENTITLED", () => { const fixtures = [ diff --git a/packages/kit/convex/purchases/android.ts b/packages/kit/convex/purchases/android.ts index 5e9a2c8a..762dd9b3 100644 --- a/packages/kit/convex/purchases/android.ts +++ b/packages/kit/convex/purchases/android.ts @@ -33,6 +33,10 @@ import { ReceiptVerificationError } from "./errors"; import { HarmonizedPurchaseState } from "./purchaseState"; import { retryOnTransient } from "./retry"; +function describeError(error: unknown): string { + return error instanceof Error ? error.name : typeof error; +} + // Google Play receipt verification action export const verifyGooglePlayReceiptInternalV1 = action({ args: { @@ -186,7 +190,7 @@ export const verifyGooglePlayReceiptInternalV1 = action({ }); } - console.error("Error verifying Android purchase:", error); + console.error("Error verifying Android purchase:", describeError(error)); throw new PlayStoreVerificationError(error); } }, @@ -250,7 +254,10 @@ function parseAndValidateServiceAccountKey( return keyData; } catch (parseError) { - console.error("Failed to parse service account key:", parseError); + console.error( + "Failed to parse service account key:", + describeError(parseError), + ); throw new InvalidServiceAccountKeyFormatError(); } } @@ -260,18 +267,30 @@ type GooglePlayVerificationResult = { remoteResponse: string; }; -function parseTimeToMillis(time?: string | null): number | undefined { - if (!time) { +export function parseTimeToMillis(time?: string | null): number | undefined { + const value = time?.trim(); + if (!value) { return undefined; } - const asNumber = Number(time); - if (!Number.isNaN(asNumber)) { - return asNumber; + if (/^\d+$/.test(value)) { + const asNumber = Number(value); + return Number.isSafeInteger(asNumber) ? asNumber : undefined; + } + + if (isNumericLikeTimestamp(value)) { + return undefined; } - const parsed = Date.parse(time); - return Number.isNaN(parsed) ? undefined : parsed; + const parsed = Date.parse(value); + return Number.isFinite(parsed) ? parsed : undefined; +} + +function isNumericLikeTimestamp(value: string): boolean { + return ( + /^[+-]?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/i.test(value) || + /^0x[0-9a-f]+$/i.test(value) + ); } export function mapSubscriptionResponseToReceiptData(args: { @@ -416,12 +435,10 @@ async function verifyPurchaseWithGooglePlay( remoteResponse = JSON.stringify(subResponse.data ?? null); } catch (subscriptionError: unknown) { - const errorMessage = - subscriptionError instanceof Error - ? subscriptionError.message - : String(subscriptionError); - console.error("Subscription verification also failed:", errorMessage); - console.error("Error details:", subscriptionError); + console.error( + "Subscription verification also failed:", + describeError(subscriptionError), + ); // Throw appropriate error based on the error type throw createPlayStoreError(subscriptionError); diff --git a/packages/kit/convex/purchases/horizon.ts b/packages/kit/convex/purchases/horizon.ts index e8ce9724..d49b1673 100644 --- a/packages/kit/convex/purchases/horizon.ts +++ b/packages/kit/convex/purchases/horizon.ts @@ -33,6 +33,12 @@ import { retryOnTransient } from "./retry"; // Docs: https://developers.meta.com/horizon/documentation/native/ps-iap const META_GRAPH_BASE = "https://graph.oculus.com"; +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; +} + export const verifyMetaHorizonReceiptInternalV1 = action({ args: { apiKey: v.string(), @@ -96,7 +102,7 @@ export const verifyMetaHorizonReceiptInternalV1 = action({ return (await res.json()) as unknown; }); } catch (error) { - const message = error instanceof Error ? error.message : String(error); + const message = describeError(error); // Persist the failure so it shows up in the dashboard, mirroring // Apple / Google paths. await ctx.runMutation(internal.purchases.internal.saveReceiptInternal, { diff --git a/packages/kit/convex/purchases/ios.ts b/packages/kit/convex/purchases/ios.ts index 9b5098ff..ab089299 100644 --- a/packages/kit/convex/purchases/ios.ts +++ b/packages/kit/convex/purchases/ios.ts @@ -34,6 +34,10 @@ import { } from "./errors"; import { retryOnTransient } from "./retry"; +function describeError(error: unknown): string { + return error instanceof Error ? error.name : typeof error; +} + export const verifyAppStoreReceiptInternalV1 = action({ args: { apiKey: v.string(), @@ -214,7 +218,7 @@ async function verifyJWSTransaction( return transactionData; } catch (error) { - console.error("Error verifying JWS transaction:", error); + console.error("Error verifying JWS transaction:", describeError(error)); throw new AppStoreTransactionVerificationFailedError( getVerificationErrorMessage(error), ); @@ -252,7 +256,7 @@ async function getAppStoreServerCredentials( ); privateKey = keyResponse.keyContent; } catch (error) { - console.error("Failed to load Apple P8 key:", error); + console.error("Failed to load Apple P8 key:", describeError(error)); missingFields.push("privateKey"); } diff --git a/packages/kit/convex/subscriptions/horizon.ts b/packages/kit/convex/subscriptions/horizon.ts index ccffb315..3703d2e9 100644 --- a/packages/kit/convex/subscriptions/horizon.ts +++ b/packages/kit/convex/subscriptions/horizon.ts @@ -30,6 +30,10 @@ import { mapWithConcurrency } from "../utils/concurrency"; const META_GRAPH_BASE = "https://graph.oculus.com"; +function describeErrorForLog(error: unknown): string { + return error instanceof Error ? error.name : typeof error; +} + type HorizonProbe = { userId: string; sku: string; @@ -109,12 +113,10 @@ export const reconcileHorizonEntitlements = internalAction({ // aggregators long-term. The purchaseToken hash is enough // to correlate this entry to the row in `subscriptions` // when an operator needs to investigate. - console.warn( - "[horizon-reconciler] check failed", - project._id, - { tokenHash: hashForLog(probe.purchaseToken) }, - error instanceof Error ? error.message : error, - ); + console.warn("[horizon-reconciler] check failed", project._id, { + tokenHash: hashForLog(probe.purchaseToken), + error: describeErrorForLog(error), + }); continue; } // Meta's response is binary: `granted: true` means the user @@ -239,7 +241,10 @@ export const reconcileHorizonNow = action({ } } catch (error) { failures += 1; - console.warn("[horizon-reconciler] check failed", error); + console.warn("[horizon-reconciler] check failed", { + tokenHash: hashForLog(probe.purchaseToken), + error: describeErrorForLog(error), + }); } } return { checked, transitioned, failures }; @@ -282,8 +287,7 @@ async function checkHorizonEntitlement(args: { clearTimeout(timeout); } if (!res.ok) { - const text = await res.text().catch(() => ""); - throw new Error(`Meta Graph API ${res.status}: ${text.slice(0, 256)}`); + throw new Error(`Meta Graph API ${res.status}`); } const body = (await res.json()) as { success?: boolean }; return body.success === true; diff --git a/packages/kit/convex/subscriptions/horizonInternal.ts b/packages/kit/convex/subscriptions/horizonInternal.ts index 464b7a85..4071d711 100644 --- a/packages/kit/convex/subscriptions/horizonInternal.ts +++ b/packages/kit/convex/subscriptions/horizonInternal.ts @@ -2,6 +2,7 @@ import { internalMutation, internalQuery } from "../_generated/server"; import { v } from "convex/values"; import type { Doc } from "../_generated/dataModel"; +import { resolveProjectByApiKeyFromDb } from "../projects/helpers"; import { applySubscriptionTransition, type CurrentSubscription, @@ -51,10 +52,8 @@ export const getProjectByApiKey = internalQuery({ }), ), handler: async (ctx, args) => { - const project = await ctx.db - .query("projects") - .withIndex("by_api_key", (q) => q.eq("apiKey", args.apiKey)) - .unique(); + const resolved = await resolveProjectByApiKeyFromDb(ctx, args.apiKey); + const project = resolved?.project ?? null; if (!project) return null; return { _id: project._id, diff --git a/packages/kit/convex/subscriptions/mutation.ts b/packages/kit/convex/subscriptions/mutation.ts index 5efc45bf..988514ca 100644 --- a/packages/kit/convex/subscriptions/mutation.ts +++ b/packages/kit/convex/subscriptions/mutation.ts @@ -2,6 +2,8 @@ import { mutation } from "../_generated/server"; import { v } from "convex/values"; import type { Doc } from "../_generated/dataModel"; +import { resolveProjectByApiKeyFromDb } from "../projects/helpers"; + // Public mutation called by SDKs after a successful receipt verification: // they know who the host-app user is, so they tell kit which userId owns // the verified purchaseToken. Idempotent — re-binding the same userId is @@ -14,10 +16,8 @@ export const bindUser = mutation({ }, returns: v.object({ ok: v.boolean(), bound: v.boolean() }), handler: async (ctx, args) => { - const project = await ctx.db - .query("projects") - .withIndex("by_api_key", (q) => q.eq("apiKey", args.apiKey)) - .unique(); + const resolved = await resolveProjectByApiKeyFromDb(ctx, args.apiKey); + const project = resolved?.project ?? null; if (!project) return { ok: false, bound: false }; const sub: Doc<"subscriptions"> | null = await ctx.db diff --git a/packages/kit/convex/subscriptions/query.ts b/packages/kit/convex/subscriptions/query.ts index 6f546056..839f3c2d 100644 --- a/packages/kit/convex/subscriptions/query.ts +++ b/packages/kit/convex/subscriptions/query.ts @@ -2,6 +2,10 @@ import { query, type QueryCtx } from "../_generated/server"; import { v } from "convex/values"; import type { Doc, Id } from "../_generated/dataModel"; +import { + resolveProjectByApiKeyFromDb, + resolveProjectByIdForCurrentUserFromDb, +} from "../projects/helpers"; import { monthlyMicrosForSub } from "./monthlyMicros"; import { selectMostRecentlyUpdatedSubscription } from "./selectLatest"; import { @@ -64,13 +68,39 @@ function shapeRow(sub: Doc<"subscriptions">) { } async function projectByApiKey( - ctx: { db: any }, - apiKey: string, + ctx: QueryCtx, + apiKey: string | undefined, +): Promise | null> { + if (!apiKey) return null; + const resolved = await resolveProjectByApiKeyFromDb(ctx, apiKey); + return resolved?.project ?? null; +} + +async function projectByIdForCurrentUser( + ctx: QueryCtx, + projectId: Id<"projects"> | undefined, ): Promise | null> { - return await ctx.db - .query("projects") - .withIndex("by_api_key", (q: any) => q.eq("apiKey", apiKey)) - .unique(); + if (!projectId) return null; + const resolved = await resolveProjectByIdForCurrentUserFromDb(ctx, projectId); + return resolved?.project ?? null; +} + +async function projectForReadArgs( + ctx: QueryCtx, + args: { + apiKey?: string; + projectId?: Id<"projects">; + }, +): Promise | null> { + if (args.projectId) { + return projectByIdForCurrentUser(ctx, args.projectId); + } + + if (args.apiKey !== undefined) { + return projectByApiKey(ctx, args.apiKey); + } + + throw new Error("apiKey or projectId is required."); } export interface MrrCurrencyEntry { @@ -186,7 +216,8 @@ export const entitlements = query({ // onesub's `SubscriptionStore.listFiltered` API. export const listSubscriptions = query({ args: { - apiKey: v.string(), + apiKey: v.optional(v.string()), + projectId: v.optional(v.id("projects")), state: v.optional(subscriptionStateValidator), productId: v.optional(v.string()), userId: v.optional(v.string()), @@ -197,7 +228,7 @@ export const listSubscriptions = query({ total: v.number(), }), handler: async (ctx, args) => { - const project = await projectByApiKey(ctx, args.apiKey); + const project = await projectForReadArgs(ctx, args); if (!project) return { items: [], total: 0 }; const limit = Math.min(Math.max(args.limit ?? 50, 1), 200); @@ -302,7 +333,10 @@ export const listSubscriptions = query({ // `recomputeSubscriptionStats` internal mutation populates rows for // future reads. export const metricsSummary = query({ - args: { apiKey: v.string() }, + args: { + apiKey: v.optional(v.string()), + projectId: v.optional(v.id("projects")), + }, returns: v.object({ activeSubs: v.number(), inGracePeriod: v.number(), @@ -328,7 +362,7 @@ export const metricsSummary = query({ ), }), handler: async (ctx, args) => { - const project = await projectByApiKey(ctx, args.apiKey); + const project = await projectForReadArgs(ctx, args); if (!project) { return { activeSubs: 0, @@ -511,7 +545,8 @@ const platformValidator = v.union(v.literal("IOS"), v.literal("Android")); export const getRevenueMetrics = query({ args: { - apiKey: v.string(), + apiKey: v.optional(v.string()), + projectId: v.optional(v.id("projects")), fromDay: v.string(), toDay: v.string(), // Server-side `productId` / `currency` / `platform` filters were @@ -554,7 +589,7 @@ export const getRevenueMetrics = query({ truncated: v.boolean(), }), handler: async (ctx, args) => { - const project = await projectByApiKey(ctx, args.apiKey); + const project = await projectForReadArgs(ctx, args); if (!project) { return { days: [], diff --git a/packages/kit/convex/webhooks/apple.ts b/packages/kit/convex/webhooks/apple.ts index b42aa695..97cab9c3 100644 --- a/packages/kit/convex/webhooks/apple.ts +++ b/packages/kit/convex/webhooks/apple.ts @@ -112,7 +112,10 @@ export const ingestAppleAsnIOS = action({ try { payload = await verifier.verifyAndDecodeNotification(args.signedPayload); } catch (error) { - console.error("[webhooks/apple] notification verification failed", error); + console.error( + "[webhooks/apple] notification verification failed", + error instanceof Error ? error.name : typeof error, + ); // ConvexError so the Hono `mapWebhookError` translates to 400 — // signature failure is a permanent error and a 5xx would trigger // ASN's automatic retry loop forever. Apple's "do not retry on diff --git a/packages/kit/convex/webhooks/google.ts b/packages/kit/convex/webhooks/google.ts index 41d62cf0..ee487f88 100644 --- a/packages/kit/convex/webhooks/google.ts +++ b/packages/kit/convex/webhooks/google.ts @@ -467,14 +467,13 @@ async function maybeFetchSubscriptionInfo( if (error instanceof ConvexError) { throw error; } - // Sanitized: only the error name/code/message is logged. The full + // Sanitized: only the error name is logged. The full // googleapis error object can include the original request URL with // an OAuth bearer token and the response body — neither belongs in // logs that get shipped to error aggregation. const sanitized = - error instanceof Error - ? `${error.name}: ${error.message}` - : "(unknown error type)"; + error instanceof Error ? error.name : "(unknown error type)"; + const errorTextForDetection = error instanceof Error ? error.message : ""; // Auth-shaped failures (401/403, "invalid_grant", "Invalid JWT") // typically mean the operator rotated the service account. Drop // the cached client so the next webhook re-reads the file and @@ -497,8 +496,8 @@ async function maybeFetchSubscriptionInfo( errorCode === "403"; if ( numericAuthFailure || - sanitized.includes("invalid_grant") || - sanitized.includes("Invalid JWT") + errorTextForDetection.includes("invalid_grant") || + errorTextForDetection.includes("Invalid JWT") ) { playClientCache.delete(String(projectId)); } diff --git a/packages/kit/convex/webhooks/query.ts b/packages/kit/convex/webhooks/query.ts index 76842881..d6130bf0 100644 --- a/packages/kit/convex/webhooks/query.ts +++ b/packages/kit/convex/webhooks/query.ts @@ -2,6 +2,7 @@ import { query } from "../_generated/server"; import type { Doc } from "../_generated/dataModel"; import { v } from "convex/values"; +import { resolveProjectByApiKeyFromDb } from "../projects/helpers"; import { webhookEventTypeValidator, webhookEventSourceValidator, @@ -38,10 +39,8 @@ export const findEventCursor = query({ }), ), handler: async (ctx, args) => { - const project = await ctx.db - .query("projects") - .withIndex("by_api_key", (q) => q.eq("apiKey", args.apiKey)) - .unique(); + const resolved = await resolveProjectByApiKeyFromDb(ctx, args.apiKey); + const project = resolved?.project ?? null; if (!project) return null; const event = await ctx.db @@ -87,7 +86,6 @@ const webhookEventStreamShape = v.object({ cancellationReason: v.optional(webhookCancellationReasonValidator), currency: v.optional(v.string()), priceAmountMicros: v.optional(v.number()), - rawSignedPayload: v.optional(v.string()), }); function shapeWebhookEvent(event: Doc<"webhookEvents">) { @@ -113,7 +111,6 @@ function shapeWebhookEvent(event: Doc<"webhookEvents">) { cancellationReason: event.cancellationReason, currency: event.currency, priceAmountMicros: event.priceAmountMicros, - rawSignedPayload: event.rawSignedPayload, }; } @@ -141,10 +138,8 @@ export const webhookEventsSince = query({ }, returns: v.array(webhookEventStreamShape), handler: async (ctx, args) => { - const project = await ctx.db - .query("projects") - .withIndex("by_api_key", (q) => q.eq("apiKey", args.apiKey)) - .unique(); + const resolved = await resolveProjectByApiKeyFromDb(ctx, args.apiKey); + const project = resolved?.project ?? null; if (!project) { // Mirror the convention used by other v1 routes: return empty @@ -222,10 +217,8 @@ export const latestWebhookEventsSince = query({ }, returns: v.array(webhookEventStreamShape), handler: async (ctx, args) => { - const project = await ctx.db - .query("projects") - .withIndex("by_api_key", (q) => q.eq("apiKey", args.apiKey)) - .unique(); + const resolved = await resolveProjectByApiKeyFromDb(ctx, args.apiKey); + const project = resolved?.project ?? null; if (!project) { return []; diff --git a/packages/kit/package.json b/packages/kit/package.json index 9318b713..657a37ef 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -40,10 +40,10 @@ "@sentry/react": "^10.49.0", "antd": "^6.1.0", "clsx": "^2.1.1", - "convex": "^1.29.2", + "convex": "^1.39.0", "google-auth-library": "^10.6.2", "googleapis": "^157.0.0", - "hono": "^4.9.9", + "hono": "^4.12.18", "hono-openapi": "^1.1.0", "lucide-react": "^0.577.0", "mixpanel-browser": "^2.72.0", @@ -62,7 +62,7 @@ "valibot": "^1.1.0" }, "devDependencies": { - "@eslint/js": "^9.21.0", + "@eslint/js": "^9.39.4", "@playwright/test": "^1.59.1", "@tailwindcss/postcss": "^4.2.4", "@testing-library/jest-dom": "^6", @@ -73,11 +73,11 @@ "@types/node": "^22.13.10", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^5.2.0", "@vitest/ui": "^4", "autoprefixer": "~10", "dotenv": "^16.4.7", - "eslint": "^9.21.0", + "eslint": "^9.39.4", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^15.15.0", @@ -90,7 +90,7 @@ "tailwindcss": "~4", "typescript": "~5.9.3", "typescript-eslint": "^8.24.1", - "vite": "^6.2.0", + "vite": "^6.4.2", "vitest": "^4" }, "lint-staged": { diff --git a/packages/kit/public/llms-full.txt b/packages/kit/public/llms-full.txt index 3fc0ebe7..4273c7e1 100644 --- a/packages/kit/public/llms-full.txt +++ b/packages/kit/public/llms-full.txt @@ -137,22 +137,23 @@ carry these headers. ## Rate limits -In-memory token bucket keyed on SHA-256(api-key). Defaults: 60-request -burst + 1 req/sec steady state (≈ 60 req/min sustained). Tunable on +In-memory token bucket keyed on SHA-256(api-key). Defaults: 600-request +burst + 10 req/sec steady state (≈ 600 req/min sustained). Tunable on self-hosted deployments via `RATE_LIMIT_CAPACITY` and `RATE_LIMIT_REFILL_PER_SEC`. The Map is capped at 10,000 entries with LRU eviction (`RATE_LIMIT_MAX_STORE`). ## Input size caps +- request body ≤ 32 KB before JSON parsing - `jws` ≤ 16 KB (Apple) - `purchaseToken` ≤ 2 KB (Google) - `userId` ≤ 256 chars (Horizon) - `sku` ≤ 256 chars (Horizon) -Oversized requests return `400 INVALID_INPUT` before IAPKit calls the -upstream store, so a misbehaving client can't burn Apple / Google / Meta -quota. +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. ## Structured logs diff --git a/packages/kit/public/llms.txt b/packages/kit/public/llms.txt index fa172f62..2f2c090f 100644 --- a/packages/kit/public/llms.txt +++ b/packages/kit/public/llms.txt @@ -15,8 +15,8 @@ Base URL: https://kit.openiap.dev Auth: `Authorization: Bearer openiap-kit_` - [POST /v1/purchase/verify](https://kit.openiap.dev/docs/api) — verify an in-app purchase; body is a tagged union on `store` -- [POST /v1/webhooks/{apiKey}](https://www.openiap.dev/docs/webhooks#setup) — lifecycle webhook receiver. Paste this URL into App Store Connect (Production + Sandbox) AND Google Cloud Pub/Sub push subscription. Auto-detects ASN v2 vs Pub/Sub by payload shape. **POST-only**; opening in a browser returns 404 — that's expected. -- [GET /v1/webhooks/stream/{apiKey}](https://www.openiap.dev/docs/webhooks#consume-stream) — long-lived SSE stream of normalized `WebhookEvent`s. Connect with `EventSource` (or the per-SDK helper); reconnects honor `Last-Event-ID`. Opening in a browser shows a blank page (text/event-stream that never closes) — use the SDK helpers or `curl -N`. +- [POST /v1/webhooks/{apiKey}](https://openiap.dev/docs/webhooks#setup) — lifecycle webhook receiver. Paste this URL into App Store Connect (Production + Sandbox) AND Google Cloud Pub/Sub push subscription. Auto-detects ASN v2 vs Pub/Sub by payload shape. **POST-only**; opening in a browser returns 404 — that's expected. +- [GET /v1/webhooks/stream/{apiKey}](https://openiap.dev/docs/webhooks#consume-stream) — long-lived SSE stream of normalized `WebhookEvent`s. Connect with `EventSource` (or the per-SDK helper); reconnects honor `Last-Event-ID`. Opening in a browser shows a blank page (text/event-stream that never closes) — use the SDK helpers or `curl -N`. - [GET /v1/openapi](https://kit.openiap.dev/v1/openapi) — machine-readable OpenAPI spec - [GET /v1](https://kit.openiap.dev/v1) — Redoc UI for the OpenAPI spec - [GET /health](https://kit.openiap.dev/health) — liveness probe (no Convex round-trip) @@ -67,6 +67,6 @@ Harmonized `state` values (truthy `isValid`): `ENTITLED`, - [/docs/verification/horizon](https://kit.openiap.dev/docs/verification/horizon) — App ID + App Secret (write-only) - [/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://www.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 +- [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 - [/docs/ai-assistants](https://kit.openiap.dev/docs/ai-assistants) — how to point Claude / Cursor / etc. at this file - [/docs/release-notes](https://kit.openiap.dev/docs/release-notes) — changelog diff --git a/packages/kit/scripts/smoke-server.sh b/packages/kit/scripts/smoke-server.sh index 77e74178..dac173af 100755 --- a/packages/kit/scripts/smoke-server.sh +++ b/packages/kit/scripts/smoke-server.sh @@ -85,6 +85,7 @@ probe() { # must serve index.html for an unknown path. probe "/health" "200" probe "/" "200" +probe "/v1" "200" probe "/api/v1" "200" probe "/intu/project/intu/apikeys" "200" probe "/assets/missing-build-asset.js" "404" diff --git a/packages/kit/server/api/v1/middleware.test.ts b/packages/kit/server/api/v1/middleware.test.ts index fe71c367..2789cfd6 100644 --- a/packages/kit/server/api/v1/middleware.test.ts +++ b/packages/kit/server/api/v1/middleware.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "vitest"; import { Hono } from "hono"; -import { apiKeyMiddleware } from "./middleware"; +import { apiKeyMiddleware, apiKeyValidationError } from "./middleware"; function buildApp() { const app = new Hono(); @@ -106,4 +106,31 @@ describe("apiKeyMiddleware", () => { }; expect(body.errors[0].code).toBe("INVALID_API_KEY"); }); + + test("returns 403 INVALID_API_KEY when the key is oversized", async () => { + const app = buildApp(); + const response = await app.request("/verify", { + method: "POST", + headers: { Authorization: `Bearer ${"a".repeat(129)}` }, + }); + expect(response.status).toBe(403); + const body = (await response.json()) as { + errors: Array<{ code: string; message: string }>; + }; + expect(body.errors[0]).toEqual({ + code: "INVALID_API_KEY", + message: "API key is too long", + }); + }); +}); + +describe("apiKeyValidationError", () => { + test("rejects blank, malformed, and oversized keys", () => { + expect(apiKeyValidationError(" ")).toBe("API key is required"); + expect(apiKeyValidationError("openiap-kit_abc 123")).toBe( + "API key is malformed", + ); + expect(apiKeyValidationError("a".repeat(129))).toBe("API key is too long"); + expect(apiKeyValidationError("openiap-kit_abc123")).toBeNull(); + }); }); diff --git a/packages/kit/server/api/v1/middleware.ts b/packages/kit/server/api/v1/middleware.ts index 4baf8637..c4b7d2b6 100644 --- a/packages/kit/server/api/v1/middleware.ts +++ b/packages/kit/server/api/v1/middleware.ts @@ -1,5 +1,20 @@ import { createMiddleware } from "hono/factory"; +const MAX_API_KEY_LENGTH = 128; + +function isValidApiKeyLength(apiKey: string): boolean { + return apiKey.length <= MAX_API_KEY_LENGTH; +} + +export function apiKeyValidationError( + apiKey: string | undefined, +): string | null { + if (!apiKey?.trim()) return "API key is required"; + if (/\s/.test(apiKey)) return "API key is malformed"; + if (!isValidApiKeyLength(apiKey)) return "API key is too long"; + return null; +} + export const apiKeyMiddleware = createMiddleware<{ Variables: { apiKey: string; @@ -45,6 +60,21 @@ export const apiKeyMiddleware = createMiddleware<{ ); } + const validationError = apiKeyValidationError(parts[1]); + if (validationError) { + return c.json( + { + errors: [ + { + code: "INVALID_API_KEY", + message: validationError, + }, + ], + }, + 403, + ); + } + c.set("apiKey", parts[1]); await next(); diff --git a/packages/kit/server/api/v1/products.test.ts b/packages/kit/server/api/v1/products.test.ts new file mode 100644 index 00000000..98c2dab8 --- /dev/null +++ b/packages/kit/server/api/v1/products.test.ts @@ -0,0 +1,583 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { Hono } from "hono"; + +const mocks = vi.hoisted(() => ({ + query: vi.fn(), + mutation: vi.fn(), +})); + +vi.mock("@/convex", () => ({ + api: { + products: { + query: { + listProducts: "listProducts", + }, + mutation: { + upsertProduct: "upsertProduct", + setProductState: "setProductState", + removeProduct: "removeProduct", + }, + jobs: { + enqueueProductSync: "enqueueProductSync", + getSyncJobById: "getSyncJobById", + cancelProductSync: "cancelProductSync", + }, + }, + }, +})); + +vi.mock("../../convex", () => ({ + client: { + query: mocks.query, + mutation: mocks.mutation, + }, + handleConvexError: () => null, +})); + +const { productsRoutes } = await import("./products"); + +function buildApp() { + const app = new Hono(); + app.route("/products", productsRoutes); + return app; +} + +describe("productsRoutes", () => { + beforeEach(() => { + mocks.query.mockReset(); + mocks.mutation.mockReset(); + }); + + it("rejects oversized path apiKey before calling Convex", async () => { + const app = buildApp(); + const response = await app.request(`/products/${"a".repeat(129)}`); + + expect(response.status).toBe(403); + await expect(response.json()).resolves.toEqual({ + errors: [{ code: "INVALID_API_KEY", message: "API key is too long" }], + }); + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects blank path apiKey before calling Convex", async () => { + const app = buildApp(); + const response = await app.request("/products/%20%20"); + + expect(response.status).toBe(403); + await expect(response.json()).resolves.toEqual({ + errors: [{ code: "INVALID_API_KEY", message: "API key is required" }], + }); + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects oversized productId inputs before calling Convex", async () => { + const app = buildApp(); + const productId = "p".repeat(257); + + const cases = [ + app.request("/products/key", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + productId, + platform: "IOS", + type: "Subscription", + title: "Premium", + }), + }), + app.request("/products/key/state", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + productId, + platform: "IOS", + state: "Draft", + }), + }), + app.request(`/products/key/${productId}?platform=IOS`, { + method: "DELETE", + }), + ]; + + for (const responsePromise of cases) { + const response = await responsePromise; + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + errors: [ + { code: "INVALID_INPUT", message: "productId must be ≤ 256 chars" }, + ], + }); + } + + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects blank productId path params before calling Convex", async () => { + const app = buildApp(); + + const response = await app.request("/products/key/%20%20?platform=IOS", { + method: "DELETE", + }); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + errors: [ + { code: "INVALID_INPUT", message: "productId must not be empty" }, + ], + }); + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects oversized product bodies before calling Convex", async () => { + const app = buildApp(); + + const response = await app.request("/products/key", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + productId: "premium_monthly", + platform: "IOS", + type: "Subscription", + title: "Premium", + description: "x".repeat(64 * 1024), + }), + }); + + expect(response.status).toBe(413); + await expect(response.json()).resolves.toEqual({ + errors: [ + { code: "PAYLOAD_TOO_LARGE", message: "Product payload is too large" }, + ], + }); + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects oversized product content-length before reading the body", async () => { + const app = buildApp(); + + const response = await app.request("/products/key", { + method: "POST", + headers: { "content-length": String(64 * 1024 + 1) }, + }); + + expect(response.status).toBe(413); + await expect(response.json()).resolves.toEqual({ + errors: [ + { code: "PAYLOAD_TOO_LARGE", message: "Product payload is too large" }, + ], + }); + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects non-object product bodies before calling Convex", async () => { + const app = buildApp(); + const cases = [ + [ + app.request("/products/key", { + method: "POST", + headers: { "content-type": "application/json" }, + body: "null", + }), + { + code: "INVALID_INPUT", + message: "productId, platform, type, title are required", + }, + ], + [ + app.request("/products/key/state", { + method: "POST", + headers: { "content-type": "application/json" }, + body: "null", + }), + { + code: "INVALID_INPUT", + message: "productId, platform, state are required", + }, + ], + ] as const; + + for (const [responsePromise, error] of cases) { + const response = await responsePromise; + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ errors: [error] }); + } + + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects invalid product enum inputs before calling Convex", async () => { + const app = buildApp(); + + const cases = [ + [ + app.request("/products/key?platform=Web"), + { code: "INVALID_INPUT", message: "platform must be IOS|Android" }, + ], + [ + app.request("/products/key", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + productId: "premium_monthly", + platform: "IOS", + type: "Rental", + title: "Premium", + }), + }), + { + code: "INVALID_INPUT", + message: "type must be Subscription|NonConsumable|Consumable", + }, + ], + [ + app.request("/products/key/state", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + productId: "premium_monthly", + platform: "IOS", + state: "Deleted", + }), + }), + { + code: "INVALID_INPUT", + message: "state must be Draft|Ready|Active|Removed", + }, + ], + ] as const; + + for (const [responsePromise, error] of cases) { + const response = await responsePromise; + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ errors: [error] }); + } + + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects invalid product string fields before calling Convex", async () => { + const app = buildApp(); + const cases = [ + [ + { + productId: " ", + platform: "IOS", + type: "Subscription", + title: "Premium", + subscriptionGroupName: "premium_tiers", + }, + { + code: "INVALID_INPUT", + message: "productId, platform, type, title are required", + }, + ], + [ + { + productId: "premium_monthly", + platform: "IOS", + type: "Subscription", + title: " ", + subscriptionGroupName: "premium_tiers", + }, + { + code: "INVALID_INPUT", + message: "productId, platform, type, title are required", + }, + ], + [ + { + productId: "premium_monthly", + platform: "IOS", + type: "Subscription", + title: 42, + subscriptionGroupName: "premium_tiers", + }, + { code: "INVALID_INPUT", message: "title must be a string" }, + ], + [ + { + productId: "premium_monthly", + platform: "IOS", + type: "Subscription", + title: "Premium", + subscriptionGroupName: {}, + }, + { + code: "INVALID_INPUT", + message: + "description, currency, subscriptionGroupName, reviewNote, storeRef must be strings", + }, + ], + ] as const; + + for (const [body, error] of cases) { + const response = await app.request("/products/key", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ errors: [error] }); + } + + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects invalid product prices before calling Convex", async () => { + const app = buildApp(); + + const cases = [-1, 1.5, Number.MAX_SAFE_INTEGER + 1, "990000"] as const; + + for (const priceAmountMicros of cases) { + const response = await app.request("/products/key", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + productId: "premium_monthly", + platform: "IOS", + type: "Subscription", + title: "Premium", + subscriptionGroupName: "premium_tiers", + priceAmountMicros, + }), + }); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + errors: [ + { + code: "INVALID_INPUT", + message: "priceAmountMicros must be a non-negative safe integer", + }, + ], + }); + } + + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects invalid sync query params before calling Convex", async () => { + const app = buildApp(); + const cases = [ + [ + app.request("/products/key/sync/ios?direction=sideways", { + method: "POST", + }), + { + code: "INVALID_INPUT", + message: "direction must be pull|push|both|purge-local", + }, + ], + [ + app.request("/products/key/sync/ios?dryRun=banana", { + method: "POST", + }), + { + code: "INVALID_INPUT", + message: "dryRun must be true|false", + }, + ], + ] as const; + + for (const [responsePromise, error] of cases) { + const response = await responsePromise; + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ errors: [error] }); + } + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects oversized sync job ids before calling Convex", async () => { + const app = buildApp(); + const jobId = "j".repeat(257); + + const cases = [ + app.request(`/products/key/sync/jobs/${jobId}`), + app.request(`/products/key/sync/jobs/${jobId}/cancel`, { + method: "POST", + }), + ]; + + for (const responsePromise of cases) { + const response = await responsePromise; + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + errors: [ + { code: "INVALID_INPUT", message: "jobId must be ≤ 256 chars" }, + ], + }); + } + + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects blank sync job ids before calling Convex", async () => { + const app = buildApp(); + + const cases = [ + app.request("/products/key/sync/jobs/%20%20"), + app.request("/products/key/sync/jobs/%20%20/cancel", { + method: "POST", + }), + ]; + + for (const responsePromise of cases) { + const response = await responsePromise; + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + errors: [{ code: "INVALID_INPUT", message: "jobId must not be empty" }], + }); + } + + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("requires iOS subscription group names before calling Convex", async () => { + const app = buildApp(); + + const response = await app.request("/products/key", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + productId: "premium_monthly", + platform: "IOS", + type: "Subscription", + title: "Premium", + }), + }); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + errors: [ + { + code: "INVALID_INPUT", + message: + "subscriptionGroupName is required for iOS Subscription products", + }, + ], + }); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("forwards subscription metadata to Convex", async () => { + const app = buildApp(); + mocks.mutation.mockResolvedValueOnce({ id: "product-id", created: true }); + + const response = await app.request("/products/key", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + productId: "premium_monthly", + platform: "IOS", + type: "Subscription", + title: "Premium", + billingPeriod: "P1M", + subscriptionGroupName: "premium_tiers", + reviewNote: "Sandbox review note", + }), + }); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual({ + id: "product-id", + created: true, + }); + expect(mocks.mutation).toHaveBeenCalledWith("upsertProduct", { + apiKey: "key", + productId: "premium_monthly", + platform: "IOS", + type: "Subscription", + title: "Premium", + description: undefined, + priceAmountMicros: undefined, + currency: undefined, + billingPeriod: "P1M", + subscriptionGroupName: "premium_tiers", + reviewNote: "Sandbox review note", + state: undefined, + storeRef: undefined, + }); + }); + + it("does not return raw internal product mutation errors", async () => { + const app = buildApp(); + mocks.mutation.mockRejectedValueOnce( + new Error("database password leaked in stack"), + ); + + const response = await app.request("/products/key", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + productId: "premium_monthly", + platform: "IOS", + type: "Subscription", + title: "Premium", + subscriptionGroupName: "premium_tiers", + }), + }); + + expect(response.status).toBe(500); + await expect(response.json()).resolves.toEqual({ + errors: [ + { + code: "PRODUCT_UPSERT_FAILED", + message: "Product upsert failed", + }, + ], + }); + expect(mocks.mutation).toHaveBeenCalledOnce(); + }); + + it("does not return raw internal product list errors", async () => { + const app = buildApp(); + mocks.query.mockRejectedValueOnce(new Error("internal query detail")); + + const response = await app.request("/products/key"); + + expect(response.status).toBe(500); + await expect(response.json()).resolves.toEqual({ + errors: [ + { + code: "PRODUCT_LIST_FAILED", + message: "Product list failed", + }, + ], + }); + expect(mocks.query).toHaveBeenCalledOnce(); + }); + + it("does not return raw internal product delete errors", async () => { + const app = buildApp(); + mocks.mutation.mockRejectedValueOnce(new Error("internal delete detail")); + + const response = await app.request( + "/products/key/premium_monthly?platform=IOS", + { method: "DELETE" }, + ); + + expect(response.status).toBe(500); + await expect(response.json()).resolves.toEqual({ + errors: [ + { + code: "PRODUCT_REMOVE_FAILED", + message: "Product remove failed", + }, + ], + }); + expect(mocks.mutation).toHaveBeenCalledOnce(); + }); +}); diff --git a/packages/kit/server/api/v1/products.ts b/packages/kit/server/api/v1/products.ts index 1d567691..3f35af8a 100644 --- a/packages/kit/server/api/v1/products.ts +++ b/packages/kit/server/api/v1/products.ts @@ -1,8 +1,14 @@ -import { Hono } from "hono"; +import { Hono, type Context, type Next } from "hono"; import { api } from "@/convex"; import type { Id } from "@/convex"; -import { client } from "../../convex"; +import { client, handleConvexError } from "../../convex"; +import { apiKeyValidationError } from "./middleware"; +import { + isContentLengthOverLimit, + JsonBodyTooLargeError, + readJsonBodyWithLimit, +} from "./request-body"; // Catalog read/write surface mirroring onesub's @onesub/providers // admin path. The actual App Store Connect / Play Console push-sync @@ -10,82 +16,188 @@ import { client } from "../../convex"; // which the dashboard / MCP server / SDKs all share. const products = new Hono(); +const MAX_PRODUCT_ID_LENGTH = 256; +const MAX_SYNC_JOB_ID_LENGTH = 256; +const MAX_PRODUCT_BODY_BYTES = 64 * 1024; + +type ProductPlatform = "IOS" | "Android"; +type ProductType = "Subscription" | "NonConsumable" | "Consumable"; +type ProductState = "Draft" | "Ready" | "Active" | "Removed"; +type BillingPeriod = "P1W" | "P1M" | "P2M" | "P3M" | "P6M" | "P1Y"; +type SyncDirection = "pull" | "push" | "both" | "purge-local"; +const PRODUCT_PLATFORMS = new Set(["IOS", "Android"]); +const PRODUCT_TYPES = new Set([ + "Subscription", + "NonConsumable", + "Consumable", +]); +const PRODUCT_STATES = new Set(["Draft", "Ready", "Active", "Removed"]); +const BILLING_PERIODS = new Set([ + "P1W", + "P1M", + "P2M", + "P3M", + "P6M", + "P1Y", +]); +const SYNC_DIRECTIONS = new Set([ + "pull", + "push", + "both", + "purge-local", +]); + +products.use("/:apiKey", pathApiKeyGuard); +products.use("/:apiKey/*", pathApiKeyGuard); products.get("/:apiKey", async (c) => { const apiKey = c.req.param("apiKey"); const platformParam = c.req.query("platform"); - const platform = - platformParam === "IOS" || platformParam === "Android" - ? platformParam - : undefined; - const list = await client.query(api.products.query.listProducts, { - apiKey, - platform, - }); - return c.json({ products: list }); + let platform: ProductPlatform | undefined; + if (platformParam !== undefined) { + if (!isProductPlatform(platformParam)) { + return invalidInput(c, "platform must be IOS|Android"); + } + platform = platformParam; + } + try { + const list = await client.query(api.products.query.listProducts, { + apiKey, + platform, + }); + return c.json({ products: list }); + } catch (error) { + return productRouteError( + c, + error, + "PRODUCT_LIST_FAILED", + "Product list failed", + ); + } }); products.post("/:apiKey", async (c) => { const apiKey = c.req.param("apiKey"); - let body: { + let body: unknown; + try { + body = await readProductJsonBody(c.req.raw); + } catch (error) { + if (error instanceof JsonBodyTooLargeError) { + return payloadTooLarge(c); + } + return c.json( + { errors: [{ code: "INVALID_INPUT", message: "Body is not JSON" }] }, + 400, + ); + } + if (!isJsonObject(body)) { + return invalidInput(c, "productId, platform, type, title are required"); + } + const payload = body as { productId?: string; platform?: "IOS" | "Android"; - type?: "Subscription" | "NonConsumable" | "Consumable"; + type?: ProductType; title?: string; description?: string; priceAmountMicros?: number; currency?: string; - billingPeriod?: "P1W" | "P1M" | "P2M" | "P3M" | "P6M" | "P1Y"; - state?: "Draft" | "Ready" | "Active" | "Removed"; + billingPeriod?: BillingPeriod; + subscriptionGroupName?: string; + reviewNote?: string; + state?: ProductState; storeRef?: string; }; - try { - body = await c.req.json(); - } catch { - return c.json( - { errors: [{ code: "INVALID_INPUT", message: "Body is not JSON" }] }, - 400, + if ( + !isNonBlankString(payload.productId) || + !payload.platform || + !payload.type || + payload.title == null + ) { + return invalidInput(c, "productId, platform, type, title are required"); + } + if (!isValidProductIdLength(payload.productId)) { + return invalidInput(c, "productId must be ≤ 256 chars"); + } + if (!isProductPlatform(payload.platform)) { + return invalidInput(c, "platform must be IOS|Android"); + } + if (!isProductType(payload.type)) { + return invalidInput( + c, + "type must be Subscription|NonConsumable|Consumable", ); } - if (!body.productId || !body.platform || !body.type || body.title == null) { - return c.json( - { - errors: [ - { - code: "INVALID_INPUT", - message: "productId, platform, type, title are required", - }, - ], - }, - 400, + if (typeof payload.title !== "string") { + return invalidInput(c, "title must be a string"); + } + if (!payload.title.trim()) { + return invalidInput(c, "productId, platform, type, title are required"); + } + if ( + !areOptionalStrings( + payload.description, + payload.currency, + payload.subscriptionGroupName, + payload.reviewNote, + payload.storeRef, + ) + ) { + return invalidInput( + c, + "description, currency, subscriptionGroupName, reviewNote, storeRef must be strings", + ); + } + if ( + payload.billingPeriod !== undefined && + !isBillingPeriod(payload.billingPeriod) + ) { + return invalidInput(c, "billingPeriod is invalid"); + } + if ( + payload.priceAmountMicros !== undefined && + !isValidPriceAmountMicros(payload.priceAmountMicros) + ) { + return invalidInput( + c, + "priceAmountMicros must be a non-negative safe integer", + ); + } + if (payload.state !== undefined && !isProductState(payload.state)) { + return invalidInput(c, "state must be Draft|Ready|Active|Removed"); + } + if ( + payload.platform === "IOS" && + payload.type === "Subscription" && + !payload.subscriptionGroupName?.trim() + ) { + return invalidInput( + c, + "subscriptionGroupName is required for iOS Subscription products", ); } try { const result = await client.mutation(api.products.mutation.upsertProduct, { apiKey, - productId: body.productId, - platform: body.platform, - type: body.type, - title: body.title, - description: body.description, - priceAmountMicros: body.priceAmountMicros, - currency: body.currency, - billingPeriod: body.billingPeriod, - state: body.state, - storeRef: body.storeRef, + productId: payload.productId, + platform: payload.platform, + type: payload.type, + title: payload.title, + description: payload.description, + priceAmountMicros: payload.priceAmountMicros, + currency: payload.currency, + billingPeriod: payload.billingPeriod, + subscriptionGroupName: payload.subscriptionGroupName, + reviewNote: payload.reviewNote, + state: payload.state, + storeRef: payload.storeRef, }); return c.json(result); } catch (error) { - return c.json( - { - errors: [ - { - code: "PRODUCT_UPSERT_FAILED", - message: error instanceof Error ? error.message : String(error), - }, - ], - }, - 400, + return productRouteError( + c, + error, + "PRODUCT_UPSERT_FAILED", + "Product upsert failed", ); } }); @@ -96,54 +208,59 @@ products.post("/:apiKey", async (c) => { // reuse pattern silently did). products.post("/:apiKey/state", async (c) => { const apiKey = c.req.param("apiKey"); - let body: { - productId?: string; - platform?: "IOS" | "Android"; - state?: "Draft" | "Ready" | "Active" | "Removed"; - }; + let body: unknown; try { - body = await c.req.json(); - } catch { + body = await readProductJsonBody(c.req.raw); + } catch (error) { + if (error instanceof JsonBodyTooLargeError) { + return payloadTooLarge(c); + } return c.json( { errors: [{ code: "INVALID_INPUT", message: "Body is not JSON" }] }, 400, ); } - if (!body.productId || !body.platform || !body.state) { - return c.json( - { - errors: [ - { - code: "INVALID_INPUT", - message: "productId, platform, state are required", - }, - ], - }, - 400, - ); + if (!isJsonObject(body)) { + return invalidInput(c, "productId, platform, state are required"); + } + const payload = body as { + productId?: string; + platform?: ProductPlatform; + state?: ProductState; + }; + if ( + !isNonBlankString(payload.productId) || + !payload.platform || + !payload.state + ) { + return invalidInput(c, "productId, platform, state are required"); + } + if (!isValidProductIdLength(payload.productId)) { + return invalidInput(c, "productId must be ≤ 256 chars"); + } + if (!isProductPlatform(payload.platform)) { + return invalidInput(c, "platform must be IOS|Android"); + } + if (!isProductState(payload.state)) { + return invalidInput(c, "state must be Draft|Ready|Active|Removed"); } try { const result = await client.mutation( api.products.mutation.setProductState, { apiKey, - productId: body.productId, - platform: body.platform, - state: body.state, + productId: payload.productId, + platform: payload.platform, + state: payload.state, }, ); return c.json(result); } catch (error) { - return c.json( - { - errors: [ - { - code: "PRODUCT_STATE_FAILED", - message: error instanceof Error ? error.message : String(error), - }, - ], - }, - 400, + return productRouteError( + c, + error, + "PRODUCT_STATE_FAILED", + "Product state update failed", ); } }); @@ -157,24 +274,22 @@ products.post("/:apiKey/state", async (c) => { products.post("/:apiKey/sync/:platform", async (c) => { const apiKey = c.req.param("apiKey"); const platformParam = c.req.param("platform"); - const direction = - (c.req.query("direction") as - | "pull" - | "push" - | "both" - | "purge-local" - | undefined) ?? "both"; - const dryRun = c.req.query("dryRun") === "true"; + const direction = c.req.query("direction") ?? "both"; + const dryRunParam = c.req.query("dryRun"); if (platformParam !== "ios" && platformParam !== "android") { - return c.json( - { - errors: [ - { code: "INVALID_INPUT", message: "platform must be ios|android" }, - ], - }, - 400, - ); + return invalidInput(c, "platform must be ios|android"); + } + if (!isSyncDirection(direction)) { + return invalidInput(c, "direction must be pull|push|both|purge-local"); } + if ( + dryRunParam !== undefined && + dryRunParam !== "true" && + dryRunParam !== "false" + ) { + return invalidInput(c, "dryRun must be true|false"); + } + const dryRun = dryRunParam === "true"; const platform: "IOS" | "Android" = platformParam === "ios" ? "IOS" : "Android"; try { @@ -186,16 +301,11 @@ products.post("/:apiKey/sync/:platform", async (c) => { }); return c.json(result, 202); } catch (error) { - return c.json( - { - errors: [ - { - code: "PRODUCT_SYNC_ENQUEUE_FAILED", - message: error instanceof Error ? error.message : String(error), - }, - ], - }, - 400, + return productRouteError( + c, + error, + "PRODUCT_SYNC_ENQUEUE_FAILED", + "Product sync enqueue failed", ); } }); @@ -207,6 +317,12 @@ products.post("/:apiKey/sync/:platform", async (c) => { products.get("/:apiKey/sync/jobs/:jobId", async (c) => { const apiKey = c.req.param("apiKey"); const jobId = c.req.param("jobId"); + if (!isNonBlankString(jobId)) { + return invalidInput(c, "jobId must not be empty"); + } + if (!isValidSyncJobIdLength(jobId)) { + return invalidInput(c, "jobId must be ≤ 256 chars"); + } try { const job = await client.query(api.products.jobs.getSyncJobById, { apiKey, @@ -220,16 +336,11 @@ products.get("/:apiKey/sync/jobs/:jobId", async (c) => { } return c.json(job); } catch (error) { - return c.json( - { - errors: [ - { - code: "PRODUCT_SYNC_LOOKUP_FAILED", - message: error instanceof Error ? error.message : String(error), - }, - ], - }, - 400, + return productRouteError( + c, + error, + "PRODUCT_SYNC_LOOKUP_FAILED", + "Product sync lookup failed", ); } }); @@ -239,6 +350,12 @@ products.get("/:apiKey/sync/jobs/:jobId", async (c) => { products.post("/:apiKey/sync/jobs/:jobId/cancel", async (c) => { const apiKey = c.req.param("apiKey"); const jobId = c.req.param("jobId"); + if (!isNonBlankString(jobId)) { + return invalidInput(c, "jobId must not be empty"); + } + if (!isValidSyncJobIdLength(jobId)) { + return invalidInput(c, "jobId must be ≤ 256 chars"); + } try { const result = await client.mutation(api.products.jobs.cancelProductSync, { apiKey, @@ -246,16 +363,11 @@ products.post("/:apiKey/sync/jobs/:jobId/cancel", async (c) => { }); return c.json(result); } catch (error) { - return c.json( - { - errors: [ - { - code: "PRODUCT_SYNC_CANCEL_FAILED", - message: error instanceof Error ? error.message : String(error), - }, - ], - }, - 400, + return productRouteError( + c, + error, + "PRODUCT_SYNC_CANCEL_FAILED", + "Product sync cancel failed", ); } }); @@ -263,26 +375,139 @@ products.post("/:apiKey/sync/jobs/:jobId/cancel", async (c) => { products.delete("/:apiKey/:productId", async (c) => { const apiKey = c.req.param("apiKey"); const productId = c.req.param("productId"); - const platform = c.req.query("platform") as "IOS" | "Android" | undefined; - if (platform !== "IOS" && platform !== "Android") { - return c.json( - { - errors: [ - { - code: "INVALID_INPUT", - message: "platform query param required (IOS | Android)", - }, - ], - }, - 400, + const platformParam = c.req.query("platform"); + if (!isProductPlatform(platformParam)) { + return invalidInput(c, "platform query param required (IOS | Android)"); + } + if (!isNonBlankString(productId)) { + return invalidInput(c, "productId must not be empty"); + } + if (!isValidProductIdLength(productId)) { + return invalidInput(c, "productId must be ≤ 256 chars"); + } + try { + const result = await client.mutation(api.products.mutation.removeProduct, { + apiKey, + productId, + platform: platformParam, + }); + return c.json(result); + } catch (error) { + return productRouteError( + c, + error, + "PRODUCT_REMOVE_FAILED", + "Product remove failed", ); } - const result = await client.mutation(api.products.mutation.removeProduct, { - apiKey, - productId, - platform, - }); - return c.json(result); }); +function isValidProductIdLength(productId: string): boolean { + return productId.length <= MAX_PRODUCT_ID_LENGTH; +} + +function isValidSyncJobIdLength(jobId: string): boolean { + return jobId.length <= MAX_SYNC_JOB_ID_LENGTH; +} + +function isValidPriceAmountMicros(value: unknown): value is number { + return typeof value === "number" && Number.isSafeInteger(value) && value >= 0; +} + +function isProductPlatform(value: unknown): value is ProductPlatform { + return typeof value === "string" && PRODUCT_PLATFORMS.has(value); +} + +function isProductType(value: unknown): value is ProductType { + return typeof value === "string" && PRODUCT_TYPES.has(value); +} + +function isProductState(value: unknown): value is ProductState { + return typeof value === "string" && PRODUCT_STATES.has(value); +} + +function isBillingPeriod(value: unknown): value is BillingPeriod { + return typeof value === "string" && BILLING_PERIODS.has(value); +} + +function isSyncDirection(direction: string): direction is SyncDirection { + return SYNC_DIRECTIONS.has(direction); +} + +function areOptionalStrings(...values: unknown[]): boolean { + return values.every( + (value) => value === undefined || typeof value === "string", + ); +} + +function isNonBlankString(value: unknown): value is string { + return typeof value === "string" && value.trim().length > 0; +} + +function isJsonObject(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function invalidInput(c: Context, message: string) { + return c.json({ errors: [{ code: "INVALID_INPUT", message }] }, 400); +} + +function payloadTooLarge(c: Context) { + return c.json( + { + errors: [ + { code: "PAYLOAD_TOO_LARGE", message: "Product payload is too large" }, + ], + }, + 413, + ); +} + +function readProductJsonBody(request: Request) { + return readJsonBodyWithLimit( + request, + MAX_PRODUCT_BODY_BYTES, + "Product payload is too large", + ); +} + +async function pathApiKeyGuard(c: Context, next: Next) { + const validationError = apiKeyValidationError(c.req.param("apiKey")); + if (validationError) { + return c.json( + { errors: [{ code: "INVALID_API_KEY", message: validationError }] }, + 403, + ); + } + if ( + c.req.method !== "GET" && + isContentLengthOverLimit( + c.req.header("content-length"), + MAX_PRODUCT_BODY_BYTES, + ) + ) { + return payloadTooLarge(c); + } + await next(); +} + +function productRouteError( + c: Context, + error: unknown, + code: string, + fallbackMessage: string, +) { + const convexError = handleConvexError(error); + if (convexError) { + return c.json({ errors: [convexError] }, 400); + } + + console.error(`[products] ${code}`, describeErrorForLog(error)); + return c.json({ errors: [{ code, message: fallbackMessage }] }, 500); +} + +function describeErrorForLog(error: unknown): string { + return error instanceof Error ? error.name : typeof error; +} + export { products as productsRoutes }; diff --git a/packages/kit/server/api/v1/rate-limit.test.ts b/packages/kit/server/api/v1/rate-limit.test.ts index 30536ad0..13b08c0c 100644 --- a/packages/kit/server/api/v1/rate-limit.test.ts +++ b/packages/kit/server/api/v1/rate-limit.test.ts @@ -114,6 +114,10 @@ describe("parsePositiveNumber", () => { test("returns fallback for NaN, Infinity, non-numeric strings", () => { expect(parsePositiveNumber("pineapple", 60, 1)).toBe(60); + expect(parsePositiveNumber("120rps", 60, 1)).toBe(60); + expect(parsePositiveNumber("0x10", 60, 1)).toBe(60); + expect(parsePositiveNumber("1e2", 60, 1)).toBe(60); + expect(parsePositiveNumber("+1", 60, 1)).toBe(60); expect(parsePositiveNumber("NaN", 60, 1)).toBe(60); expect(parsePositiveNumber("Infinity", 60, 1)).toBe(60); }); @@ -128,6 +132,7 @@ describe("parsePositiveNumber", () => { test("returns the parsed value when it is finite and above min", () => { expect(parsePositiveNumber("120", 60, 1)).toBe(120); + expect(parsePositiveNumber(" 120 ", 60, 1)).toBe(120); expect(parsePositiveNumber("0.25", 1, 0.001)).toBe(0.25); }); }); diff --git a/packages/kit/server/api/v1/replay-guard.test.ts b/packages/kit/server/api/v1/replay-guard.test.ts index a7d472b0..988889de 100644 --- a/packages/kit/server/api/v1/replay-guard.test.ts +++ b/packages/kit/server/api/v1/replay-guard.test.ts @@ -111,6 +111,27 @@ describe("markPayloadFailure + tryConsumeReplay cooldown", () => { expect(blocked.retryAfterSec).toBeGreaterThanOrEqual(58); }); + test("does not extend cooldown retry-after when the clock moves backward", () => { + const store = new Map(); + const now = 10_000; + const cooldownMs = 60_000; + + markPayloadFailure(store, "k:p", 30, now, 100); + const blocked = tryConsumeReplay( + store, + "k:p", + 30, + 1, + now - 5_000, + 100, + cooldownMs, + ); + + expect(blocked.allowed).toBe(false); + expect(blocked.reason).toBe("repeated_failure"); + expect(blocked.retryAfterSec).toBe(60); + }); + test("allows the same payload again after the cooldown elapses", () => { const store = new Map(); let now = 1_000; diff --git a/packages/kit/server/api/v1/replay-guard.ts b/packages/kit/server/api/v1/replay-guard.ts index 7edfb799..b6314d15 100644 --- a/packages/kit/server/api/v1/replay-guard.ts +++ b/packages/kit/server/api/v1/replay-guard.ts @@ -48,8 +48,8 @@ export interface ReplayGuardConfig { refillPerSecond: number; maxStoreSize: number; /** Cooldown after a failed verification of the same payload. Defaults - * are tuned for the common case where Apple / Google's verdict for a - * given receipt is stable for far longer than this window. */ + * are tuned for the common case where the store provider's verdict for + * a given receipt is stable for far longer than this window. */ failureCooldownMs: number; now?: () => number; store?: Map; @@ -130,20 +130,19 @@ export function tryConsumeReplay( // while the cooldown is active, even if the bucket happens to have // tokens. This is the layer that defeats "captured-then-revoked // receipt replay": the attacker has a real-shaped receipt that - // Apple / Google said no to, and trying again 200 ms later just + // the store provider said no to, and trying again 200 ms later just // burns our upstream quota for the same answer. - if ( - failureCooldownMs > 0 && - bucket.lastFailureMs !== undefined && - nowMs - bucket.lastFailureMs < failureCooldownMs - ) { - const remainingMs = failureCooldownMs - (nowMs - bucket.lastFailureMs); - return { - allowed: false, - remaining: 0, - retryAfterSec: Math.max(1, Math.ceil(remainingMs / 1000)), - reason: "repeated_failure", - }; + if (failureCooldownMs > 0 && bucket.lastFailureMs !== undefined) { + const elapsedSinceFailureMs = Math.max(0, nowMs - bucket.lastFailureMs); + if (elapsedSinceFailureMs < failureCooldownMs) { + const remainingMs = failureCooldownMs - elapsedSinceFailureMs; + return { + allowed: false, + remaining: 0, + retryAfterSec: Math.max(1, Math.ceil(remainingMs / 1000)), + reason: "repeated_failure", + }; + } } const elapsedSec = Math.max(0, (nowMs - bucket.lastRefillMs) / 1000); @@ -231,9 +230,9 @@ const DEFAULT_MAX_STORE_SIZE = parsePositiveNumber( // Default failure cooldown: 5 minutes. Long enough that "replay the // same revoked receipt" attacks see a hard wall well past any // reasonable client-side retry-on-transient cadence; short enough -// that if Apple / Google really did re-validate a previously-failed -// receipt (rare but possible during outages), the client recovers -// within one app session. +// that if the store provider really did re-validate a previously- +// failed receipt (rare but possible during outages), the client +// recovers within one app session. const DEFAULT_FAILURE_COOLDOWN_MS = parsePositiveNumber(process.env.REPLAY_GUARD_FAILURE_COOLDOWN_SEC, 300, 1) * 1000; @@ -305,7 +304,7 @@ export function replayGuardMiddleware( : "DUPLICATE_PAYLOAD"; const message = result.reason === "repeated_failure" - ? `This receipt was just rejected as invalid by the upstream store; the same payload won't be re-verified for ${result.retryAfterSec}s. If you believe this is wrong, wait the cooldown then retry — Apple / Google's verdict for a given receipt almost never changes within seconds.` + ? `This receipt was just rejected as invalid by the upstream store; the same payload won't be re-verified for ${result.retryAfterSec}s. If you believe this is wrong, wait the cooldown then retry — the store provider's verdict for a given receipt almost never changes within seconds.` : `Too many verifications for the same payload from this API key. Legitimate clients re-verify a receipt at most a handful of times per minute. Retry after ${result.retryAfterSec}s, or cache the previous result on your side.`; return c.json( { diff --git a/packages/kit/server/api/v1/request-body.test.ts b/packages/kit/server/api/v1/request-body.test.ts new file mode 100644 index 00000000..d41590b3 --- /dev/null +++ b/packages/kit/server/api/v1/request-body.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; + +import { isContentLengthOverLimit } from "./request-body"; + +describe("isContentLengthOverLimit", () => { + it("compares decimal content-length values against the byte limit", () => { + expect(isContentLengthOverLimit("1024", 1024)).toBe(false); + expect(isContentLengthOverLimit(" 1025 ", 1024)).toBe(true); + expect(isContentLengthOverLimit("9".repeat(100), 1024)).toBe(true); + }); + + it("ignores malformed non-decimal content-length values", () => { + expect(isContentLengthOverLimit(undefined, 1024)).toBe(false); + expect(isContentLengthOverLimit("", 1024)).toBe(false); + expect(isContentLengthOverLimit("+1025", 1024)).toBe(false); + expect(isContentLengthOverLimit("0x401", 1024)).toBe(false); + expect(isContentLengthOverLimit("1025abc", 1024)).toBe(false); + expect(isContentLengthOverLimit("-1", 1024)).toBe(false); + }); +}); diff --git a/packages/kit/server/api/v1/request-body.ts b/packages/kit/server/api/v1/request-body.ts new file mode 100644 index 00000000..4775f551 --- /dev/null +++ b/packages/kit/server/api/v1/request-body.ts @@ -0,0 +1,64 @@ +export class JsonBodyTooLargeError extends Error { + constructor(message = "Request body is too large") { + super(message); + } +} + +export function isContentLengthOverLimit( + contentLengthHeader: string | undefined, + limitBytes: number, +): boolean { + if (!contentLengthHeader) return false; + const value = contentLengthHeader.trim(); + if (!/^\d+$/.test(value)) return false; + try { + return BigInt(value) > BigInt(limitBytes); + } catch { + return false; + } +} + +export async function readJsonBodyWithLimit( + request: Request, + limitBytes: number, + errorMessage = "Request body is too large", +): Promise { + const text = await readRequestTextWithLimit( + request, + limitBytes, + errorMessage, + ); + return JSON.parse(text); +} + +async function readRequestTextWithLimit( + request: Request, + limitBytes: number, + errorMessage: string, +): Promise { + const reader = request.body?.getReader(); + if (!reader) return ""; + + const chunks: Uint8Array[] = []; + let totalBytes = 0; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + if (!value) continue; + totalBytes += value.byteLength; + if (totalBytes > limitBytes) { + await reader.cancel().catch(() => undefined); + throw new JsonBodyTooLargeError(errorMessage); + } + chunks.push(value); + } + + const bytes = new Uint8Array(totalBytes); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + return new TextDecoder().decode(bytes); +} diff --git a/packages/kit/server/api/v1/request-logger.test.ts b/packages/kit/server/api/v1/request-logger.test.ts index fcc6067e..7fda3cdb 100644 --- a/packages/kit/server/api/v1/request-logger.test.ts +++ b/packages/kit/server/api/v1/request-logger.test.ts @@ -17,6 +17,8 @@ import { verifyPurchaseInputSchema } from "./route-input-schemas"; // the downstream handler behavior. 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"; type TestVars = { apiKey?: string; @@ -144,6 +146,28 @@ describe("requestLoggerMiddleware", () => { expect(logs[0].state).toBe("INAUTHENTIC"); }); + test("logs Horizon verification store values", async () => { + const logs: VerifyLogLine[] = []; + const app = buildApp({ logs }); + + const res = await app.request("/verify", { + method: "POST", + headers: { + Authorization: "Bearer key-horizon", + "content-type": "application/json", + }, + body: JSON.stringify({ + store: "horizon", + userId: TEST_HORIZON_USER_ID, + sku: TEST_HORIZON_SKU, + }), + }); + + expect(res.status).toBe(200); + expect(logs).toHaveLength(1); + expect(logs[0].store).toBe("horizon"); + }); + test("populates the X-Correlation-Id response header even on validator failure", async () => { const logs: VerifyLogLine[] = []; const app = buildApp({ logs }); @@ -220,6 +244,7 @@ describe("requestLoggerMiddleware", () => { expect(res.status).toBeGreaterThanOrEqual(500); expect(logs).toHaveLength(1); expect(logs[0].corrId).toBe("corr-fixed"); + expect(logs[0].statusCode).toBe(500); expect(logs[0].apiKeyHash).toBeDefined(); expect(logs[0].store).toBe("apple"); }); diff --git a/packages/kit/server/api/v1/request-logger.ts b/packages/kit/server/api/v1/request-logger.ts index 505376ea..cd4d13e1 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"; +export type VerifyStore = "apple" | "google" | "horizon"; export interface VerifyOutcome { isValid: boolean; @@ -37,6 +37,10 @@ export const defaultVerifyLogger: VerifyLogger = (line) => { console.log(JSON.stringify({ level: "info", ...line })); }; +function describeErrorForLog(error: unknown): string { + return error instanceof Error ? error.name : typeof error; +} + export interface RequestLoggerConfig { logger?: VerifyLogger; now?: () => number; @@ -70,8 +74,12 @@ export function requestLoggerMiddleware( // swallow the log line — the 5xx paths are exactly when we most // want structured context, and the error itself will re-throw after // the finally runs. + let nextError: unknown; try { await next(); + } catch (error) { + nextError = error; + throw error; } finally { const durationMs = clock() - start; @@ -104,7 +112,7 @@ export function requestLoggerMiddleware( corrId, method: c.req.method, path: c.req.path, - statusCode: c.res.status, + statusCode: nextError && c.res.status < 400 ? 500 : c.res.status, durationMs, apiKeyHash, store, @@ -112,7 +120,10 @@ export function requestLoggerMiddleware( state: outcome?.state, }); } catch (loggerError) { - console.error("request-logger failed:", loggerError); + console.error( + "request-logger failed:", + describeErrorForLog(loggerError), + ); } } }); diff --git a/packages/kit/server/api/v1/routes.ts b/packages/kit/server/api/v1/routes.ts index e8c4824f..77542f02 100644 --- a/packages/kit/server/api/v1/routes.ts +++ b/packages/kit/server/api/v1/routes.ts @@ -229,9 +229,10 @@ const verifyPurchaseRouteDescription = describeRoute({ "`X-Correlation-Id`. 401 / 403 responses from the auth layer run " + "before the rate-limit middleware and do not include those " + "headers.\n\n" + - "Input size caps: `jws` ≤ 16 KB, `purchaseToken` ≤ 2 KB, " + - "`userId` ≤ 256 chars, `sku` ≤ 256 chars. Oversized payloads return " + - "`400 INVALID_INPUT` without hitting the upstream store.", + "Input size caps: request body ≤ 32 KB, `jws` ≤ 16 KB, " + + "`purchaseToken` ≤ 2 KB, `userId` ≤ 256 chars, `sku` ≤ 256 chars. " + + "Oversized fields return `400 INVALID_INPUT`; oversized request " + + "bodies return `413 PAYLOAD_TOO_LARGE`. Neither hits the upstream store.", security: [{ apiKey: [] }], responses: { 200: { @@ -253,6 +254,16 @@ const verifyPurchaseRouteDescription = describeRoute({ }, }, }, + 413: { + description: + "Request body exceeds the 32 KB edge cap (`PAYLOAD_TOO_LARGE`).", + headers: commonResponseHeaders, + content: { + "application/json": { + schema: resolver(apiErrorResponseSchema), + }, + }, + }, 401: { description: "Missing bearer token", content: { @@ -282,7 +293,7 @@ const verifyPurchaseRouteDescription = describeRoute({ " • `REPEATED_FAILURE` — the exact same receipt was just " + "rejected as invalid by the upstream store; subsequent " + "requests for that payload are short-circuited for a 5-minute " + - "cooldown. Apple / Google's verdict for a given receipt rarely " + + "cooldown. The store provider's verdict for a given receipt rarely " + "changes within seconds, so the cached negative spares both " + "your quota and the upstream API. Retry after `Retry-After`.\n\n" + "Response body: `{ errors: [{ code, message, path? }] }`.", @@ -396,10 +407,11 @@ const verifyPurchaseHandler = async ( } const errorId = crypto.randomUUID(); + const errorType = error instanceof Error ? error.name : typeof error; console.error( "Unexpected error (%s) when verifying purchase: %s", errorId, - error, + errorType, ); return c.json( @@ -422,7 +434,8 @@ const verifyReplayGuard = replayGuardMiddleware(); // Middleware order matters: // 1. apiKeyMiddleware — 401/403 before anything expensive. -// 2. verifyRequestLogger — logs every attempt for audit/debug. +// 2. verifyRequestLogger — logs every attempt that passed auth-header +// shape validation for audit/debug. // 3. verifyRateLimit — per-key burst cap; also populates `apiKeyHash`. // 4. validator — rejects malformed payloads (400) before the guard // below hashes the body. diff --git a/packages/kit/server/api/v1/subscriptions.test.ts b/packages/kit/server/api/v1/subscriptions.test.ts new file mode 100644 index 00000000..1eaeb0ce --- /dev/null +++ b/packages/kit/server/api/v1/subscriptions.test.ts @@ -0,0 +1,354 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { Hono } from "hono"; + +const mocks = vi.hoisted(() => ({ + query: vi.fn(), + mutation: vi.fn(), +})); + +vi.mock("@/convex", () => ({ + api: { + subscriptions: { + query: { + subscriptionStatus: "subscriptionStatus", + entitlements: "entitlements", + listSubscriptions: "listSubscriptions", + metricsSummary: "metricsSummary", + }, + mutation: { + bindUser: "bindUser", + }, + }, + }, +})); + +vi.mock("../../convex", () => ({ + client: { + query: mocks.query, + mutation: mocks.mutation, + }, + handleConvexError: () => null, +})); + +const { subscriptionsRoutes } = await import("./subscriptions"); + +function buildApp() { + const app = new Hono(); + app.route("/subscriptions", subscriptionsRoutes); + return app; +} + +describe("subscriptionsRoutes", () => { + beforeEach(() => { + mocks.query.mockReset(); + mocks.mutation.mockReset(); + }); + + it("rejects oversized path apiKey before calling Convex", async () => { + const app = buildApp(); + const response = await app.request( + `/subscriptions/status/${"a".repeat(129)}?userId=user-1`, + ); + + expect(response.status).toBe(403); + await expect(response.json()).resolves.toEqual({ + errors: [{ code: "INVALID_API_KEY", message: "API key is too long" }], + }); + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects blank path apiKey before calling Convex", async () => { + const app = buildApp(); + const response = await app.request( + "/subscriptions/status/%20%20?userId=user-1", + ); + + expect(response.status).toBe(403); + await expect(response.json()).resolves.toEqual({ + errors: [{ code: "INVALID_API_KEY", message: "API key is required" }], + }); + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects oversized userId inputs before calling Convex", async () => { + const app = buildApp(); + const userId = "u".repeat(257); + + const cases = [ + app.request(`/subscriptions/status/key?userId=${userId}`), + app.request(`/subscriptions/entitlements/key?userId=${userId}`), + app.request(`/subscriptions/list/key?userId=${userId}`), + app.request("/subscriptions/bind-user/key", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ purchaseToken: "token", userId }), + }), + ]; + + for (const responsePromise of cases) { + const response = await responsePromise; + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + errors: [ + { code: "INVALID_INPUT", message: "userId must be ≤ 256 chars" }, + ], + }); + } + + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects blank query userId inputs before calling Convex", async () => { + const app = buildApp(); + + const cases = [ + app.request("/subscriptions/status/key?userId=%20%20"), + app.request("/subscriptions/entitlements/key?userId=%20%20"), + ]; + + for (const responsePromise of cases) { + const response = await responsePromise; + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + errors: [{ code: "INVALID_INPUT", message: "userId is required" }], + }); + } + + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects invalid list filters before calling Convex", async () => { + const app = buildApp(); + + const cases = [ + [ + "/subscriptions/list/key?state=Deleted", + { code: "INVALID_INPUT", message: "state is invalid" }, + ], + [ + "/subscriptions/list/key?userId=", + { code: "INVALID_INPUT", message: "userId must not be empty" }, + ], + [ + "/subscriptions/list/key?userId=%20%20", + { code: "INVALID_INPUT", message: "userId must not be empty" }, + ], + [ + "/subscriptions/list/key?productId=", + { code: "INVALID_INPUT", message: "productId must not be empty" }, + ], + [ + "/subscriptions/list/key?productId=%20%20", + { code: "INVALID_INPUT", message: "productId must not be empty" }, + ], + [ + `/subscriptions/list/key?productId=${"p".repeat(257)}`, + { code: "INVALID_INPUT", message: "productId must be ≤ 256 chars" }, + ], + [ + "/subscriptions/list/key?limit=abc", + { + code: "INVALID_INPUT", + message: "limit must be a positive integer", + }, + ], + [ + "/subscriptions/list/key?limit=", + { + code: "INVALID_INPUT", + message: "limit must be a positive integer", + }, + ], + [ + "/subscriptions/list/key?limit=0", + { + code: "INVALID_INPUT", + message: "limit must be a positive integer", + }, + ], + [ + "/subscriptions/list/key?limit=1.5", + { + code: "INVALID_INPUT", + message: "limit must be a positive integer", + }, + ], + [ + "/subscriptions/list/key?limit=1e2", + { + code: "INVALID_INPUT", + message: "limit must be a positive integer", + }, + ], + ] as const; + + for (const [url, error] of cases) { + const response = await app.request(url); + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ errors: [error] }); + } + + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects oversized bind-user purchaseToken before calling Convex", async () => { + const app = buildApp(); + const response = await app.request("/subscriptions/bind-user/key", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + purchaseToken: "t".repeat(2_001), + userId: "user-1", + }), + }); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + errors: [ + { + code: "INVALID_INPUT", + message: "purchaseToken must be ≤ 2000 chars", + }, + ], + }); + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects oversized bind-user bodies before calling Convex", async () => { + const app = buildApp(); + const response = await app.request("/subscriptions/bind-user/key", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + purchaseToken: "token", + userId: "user-1", + padding: "x".repeat(8 * 1024), + }), + }); + + expect(response.status).toBe(413); + await expect(response.json()).resolves.toEqual({ + errors: [ + { + code: "PAYLOAD_TOO_LARGE", + message: "Subscription payload is too large", + }, + ], + }); + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects oversized bind-user content-length before reading the body", async () => { + const app = buildApp(); + const response = await app.request("/subscriptions/bind-user/key", { + method: "POST", + headers: { "content-length": String(8 * 1024 + 1) }, + }); + + expect(response.status).toBe(413); + await expect(response.json()).resolves.toEqual({ + errors: [ + { + code: "PAYLOAD_TOO_LARGE", + message: "Subscription payload is too large", + }, + ], + }); + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects non-object bind-user bodies before calling Convex", async () => { + const app = buildApp(); + const response = await app.request("/subscriptions/bind-user/key", { + method: "POST", + headers: { "content-type": "application/json" }, + body: "null", + }); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + errors: [ + { + code: "INVALID_INPUT", + message: "purchaseToken and userId are required", + }, + ], + }); + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("rejects blank bind-user strings before calling Convex", async () => { + const app = buildApp(); + const response = await app.request("/subscriptions/bind-user/key", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ purchaseToken: " ", userId: "user-1" }), + }); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + errors: [ + { + code: "INVALID_INPUT", + message: "purchaseToken and userId are required", + }, + ], + }); + expect(mocks.query).not.toHaveBeenCalled(); + expect(mocks.mutation).not.toHaveBeenCalled(); + }); + + it("does not return raw internal subscription query errors", async () => { + const app = buildApp(); + mocks.query.mockRejectedValueOnce(new Error("internal query detail")); + + const response = await app.request( + "/subscriptions/status/key?userId=user-1", + ); + + expect(response.status).toBe(500); + await expect(response.json()).resolves.toEqual({ + errors: [ + { + code: "SUBSCRIPTION_STATUS_FAILED", + message: "Subscription status lookup failed", + }, + ], + }); + expect(mocks.query).toHaveBeenCalledOnce(); + }); + + it("does not return raw internal bind-user mutation errors", async () => { + const app = buildApp(); + mocks.mutation.mockRejectedValueOnce(new Error("internal mutation detail")); + + const response = await app.request("/subscriptions/bind-user/key", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + purchaseToken: "token", + userId: "user-1", + }), + }); + + expect(response.status).toBe(500); + await expect(response.json()).resolves.toEqual({ + errors: [ + { + code: "SUBSCRIPTION_BIND_USER_FAILED", + message: "Subscription user binding failed", + }, + ], + }); + expect(mocks.mutation).toHaveBeenCalledOnce(); + }); +}); diff --git a/packages/kit/server/api/v1/subscriptions.ts b/packages/kit/server/api/v1/subscriptions.ts index 5872bfdd..ea0a9e66 100644 --- a/packages/kit/server/api/v1/subscriptions.ts +++ b/packages/kit/server/api/v1/subscriptions.ts @@ -1,7 +1,13 @@ -import { Hono } from "hono"; +import { Hono, type Context, type Next } from "hono"; import { api } from "@/convex"; -import { client } from "../../convex"; +import { client, handleConvexError } from "../../convex"; +import { apiKeyValidationError } from "./middleware"; +import { + isContentLengthOverLimit, + JsonBodyTooLargeError, + readJsonBodyWithLimit, +} from "./request-body"; // Subscription state, entitlements, metrics, and user-binding routes. // Mirrors the role of onesub's `/onesub/status`, `/onesub/admin/...` @@ -10,113 +16,299 @@ import { client } from "../../convex"; // fetch implementations that strip them. const subscriptions = new Hono(); +const MAX_USER_ID_LENGTH = 256; +const MAX_PRODUCT_ID_LENGTH = 256; +const MAX_PURCHASE_TOKEN_LENGTH = 2_000; +const MAX_BIND_USER_BODY_BYTES = 8 * 1024; +type SubscriptionState = + | "Active" + | "InGracePeriod" + | "InBillingRetry" + | "Expired" + | "Revoked" + | "Refunded" + | "Paused" + | "Unknown"; +const SUBSCRIPTION_STATES = new Set([ + "Active", + "InGracePeriod", + "InBillingRetry", + "Expired", + "Revoked", + "Refunded", + "Paused", + "Unknown", +]); +const API_KEY_ROUTES = [ + "/status/:apiKey", + "/entitlements/:apiKey", + "/list/:apiKey", + "/metrics/:apiKey", + "/bind-user/:apiKey", +]; + +for (const route of API_KEY_ROUTES) { + subscriptions.use(route, pathApiKeyGuard); +} subscriptions.get("/status/:apiKey", async (c) => { const apiKey = c.req.param("apiKey"); const userId = c.req.query("userId"); - if (!userId) { - return c.json( - { errors: [{ code: "INVALID_INPUT", message: "userId is required" }] }, - 400, - ); + if (!isNonBlankString(userId)) { + return invalidInput(c, "userId is required"); } - if (userId.length > 256) { - return c.json( + if (!isValidUserIdLength(userId)) { + return invalidInput(c, "userId must be ≤ 256 chars"); + } + try { + const result = await client.query( + api.subscriptions.query.subscriptionStatus, { - errors: [ - { code: "INVALID_INPUT", message: "userId must be ≤ 256 chars" }, - ], + apiKey, + userId, }, - 400, + ); + return c.json(result); + } catch (error) { + return subscriptionRouteError( + c, + error, + "SUBSCRIPTION_STATUS_FAILED", + "Subscription status lookup failed", ); } - const result = await client.query( - api.subscriptions.query.subscriptionStatus, - { - apiKey, - userId, - }, - ); - return c.json(result); }); subscriptions.get("/entitlements/:apiKey", async (c) => { const apiKey = c.req.param("apiKey"); const userId = c.req.query("userId"); - if (!userId) { - return c.json( - { errors: [{ code: "INVALID_INPUT", message: "userId is required" }] }, - 400, + if (!isNonBlankString(userId)) { + return invalidInput(c, "userId is required"); + } + if (!isValidUserIdLength(userId)) { + return invalidInput(c, "userId must be ≤ 256 chars"); + } + try { + const result = await client.query(api.subscriptions.query.entitlements, { + apiKey, + userId, + }); + return c.json(result); + } catch (error) { + return subscriptionRouteError( + c, + error, + "SUBSCRIPTION_ENTITLEMENTS_FAILED", + "Subscription entitlements lookup failed", ); } - const result = await client.query(api.subscriptions.query.entitlements, { - apiKey, - userId, - }); - return c.json(result); }); subscriptions.get("/list/:apiKey", async (c) => { const apiKey = c.req.param("apiKey"); - const state = c.req.query("state"); + const stateParam = c.req.query("state"); const productId = c.req.query("productId"); const userId = c.req.query("userId"); const limit = parseLimit(c.req.query("limit")); - const result = await client.query(api.subscriptions.query.listSubscriptions, { - apiKey, - state: state as never, - productId: productId ?? undefined, - userId: userId ?? undefined, - limit, - }); - return c.json(result); + if (limit === null) { + return invalidInput(c, "limit must be a positive integer"); + } + if (userId !== undefined && !isNonBlankString(userId)) { + return invalidInput(c, "userId must not be empty"); + } + if (productId !== undefined && !isNonBlankString(productId)) { + return invalidInput(c, "productId must not be empty"); + } + if (userId !== undefined && !isValidUserIdLength(userId)) { + return invalidInput(c, "userId must be ≤ 256 chars"); + } + if (productId !== undefined && !isValidProductIdLength(productId)) { + return invalidInput(c, "productId must be ≤ 256 chars"); + } + let state: SubscriptionState | undefined; + if (stateParam !== undefined) { + if (!isSubscriptionState(stateParam)) { + return invalidInput(c, "state is invalid"); + } + state = stateParam; + } + try { + const result = await client.query( + api.subscriptions.query.listSubscriptions, + { + apiKey, + state, + productId: productId ?? undefined, + userId: userId ?? undefined, + limit, + }, + ); + return c.json(result); + } catch (error) { + return subscriptionRouteError( + c, + error, + "SUBSCRIPTION_LIST_FAILED", + "Subscription list failed", + ); + } }); subscriptions.get("/metrics/:apiKey", async (c) => { const apiKey = c.req.param("apiKey"); - const result = await client.query(api.subscriptions.query.metricsSummary, { - apiKey, - }); - return c.json(result); + try { + const result = await client.query(api.subscriptions.query.metricsSummary, { + apiKey, + }); + return c.json(result); + } catch (error) { + return subscriptionRouteError( + c, + error, + "SUBSCRIPTION_METRICS_FAILED", + "Subscription metrics lookup failed", + ); + } }); subscriptions.post("/bind-user/:apiKey", async (c) => { const apiKey = c.req.param("apiKey"); - let body: { purchaseToken?: string; userId?: string }; + let body: unknown; try { - body = await c.req.json<{ purchaseToken?: string; userId?: string }>(); - } catch { + body = await readJsonBodyWithLimit( + c.req.raw, + MAX_BIND_USER_BODY_BYTES, + "Subscription payload is too large", + ); + } catch (error) { + if (error instanceof JsonBodyTooLargeError) { + return payloadTooLarge(c); + } return c.json( { errors: [{ code: "INVALID_INPUT", message: "Body is not JSON" }] }, 400, ); } - if (!body.purchaseToken || !body.userId) { - return c.json( - { - errors: [ - { - code: "INVALID_INPUT", - message: "purchaseToken and userId are required", - }, - ], - }, - 400, + if (!isJsonObject(body)) { + return invalidInput(c, "purchaseToken and userId are required"); + } + const payload = body as { purchaseToken?: string; userId?: string }; + if ( + !isNonBlankString(payload.purchaseToken) || + !isNonBlankString(payload.userId) + ) { + return invalidInput(c, "purchaseToken and userId are required"); + } + if (!isValidPurchaseTokenLength(payload.purchaseToken)) { + return invalidInput(c, "purchaseToken must be ≤ 2000 chars"); + } + if (!isValidUserIdLength(payload.userId)) { + return invalidInput(c, "userId must be ≤ 256 chars"); + } + try { + const result = await client.mutation(api.subscriptions.mutation.bindUser, { + apiKey, + purchaseToken: payload.purchaseToken, + userId: payload.userId, + }); + return c.json(result); + } catch (error) { + return subscriptionRouteError( + c, + error, + "SUBSCRIPTION_BIND_USER_FAILED", + "Subscription user binding failed", ); } - const result = await client.mutation(api.subscriptions.mutation.bindUser, { - apiKey, - purchaseToken: body.purchaseToken, - userId: body.userId, - }); - return c.json(result); }); -function parseLimit(raw: string | undefined): number | undefined { - if (!raw) return undefined; +function parseLimit(raw: string | undefined): number | undefined | null { + if (raw === undefined) return undefined; + if (!/^\d+$/.test(raw)) return null; const n = Number(raw); - if (!Number.isFinite(n) || n <= 0) return undefined; - return Math.min(Math.max(Math.trunc(n), 1), 200); + if (!Number.isFinite(n) || n <= 0 || !Number.isInteger(n)) return null; + return Math.min(n, 200); +} + +function isValidUserIdLength(userId: string): boolean { + return userId.length <= MAX_USER_ID_LENGTH; +} + +function isValidProductIdLength(productId: string): boolean { + return productId.length <= MAX_PRODUCT_ID_LENGTH; +} + +function isValidPurchaseTokenLength(purchaseToken: string): boolean { + return purchaseToken.length <= MAX_PURCHASE_TOKEN_LENGTH; +} + +function isSubscriptionState(state: string): state is SubscriptionState { + return SUBSCRIPTION_STATES.has(state); +} + +function isJsonObject(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function isNonBlankString(value: unknown): value is string { + return typeof value === "string" && value.trim().length > 0; +} + +function invalidInput(c: Context, message: string) { + return c.json({ errors: [{ code: "INVALID_INPUT", message }] }, 400); +} + +function payloadTooLarge(c: Context) { + return c.json( + { + errors: [ + { + code: "PAYLOAD_TOO_LARGE", + message: "Subscription payload is too large", + }, + ], + }, + 413, + ); +} + +async function pathApiKeyGuard(c: Context, next: Next) { + const validationError = apiKeyValidationError(c.req.param("apiKey")); + if (validationError) { + return c.json( + { errors: [{ code: "INVALID_API_KEY", message: validationError }] }, + 403, + ); + } + if ( + c.req.method !== "GET" && + isContentLengthOverLimit( + c.req.header("content-length"), + MAX_BIND_USER_BODY_BYTES, + ) + ) { + return payloadTooLarge(c); + } + await next(); +} + +function subscriptionRouteError( + c: Context, + error: unknown, + code: string, + fallbackMessage: string, +) { + const convexError = handleConvexError(error); + if (convexError) { + return c.json({ errors: [convexError] }, 400); + } + + console.error(`[subscriptions] ${code}`, describeErrorForLog(error)); + return c.json({ errors: [{ code, message: fallbackMessage }] }, 500); +} + +function describeErrorForLog(error: unknown): string { + return error instanceof Error ? error.name : typeof error; } export { subscriptions as subscriptionsRoutes }; diff --git a/packages/kit/server/api/v1/validator.test.ts b/packages/kit/server/api/v1/validator.test.ts index f82c5fab..f7d95ce7 100644 --- a/packages/kit/server/api/v1/validator.test.ts +++ b/packages/kit/server/api/v1/validator.test.ts @@ -14,7 +14,7 @@ const schema = v.object({ function buildApp() { const app = new Hono(); app.post("/echo", validator(schema), (c) => { - const json = c.req.valid("json"); + const json = c.req.valid("json" as never); return c.json({ ok: true, echo: json }); }); return app; @@ -38,6 +38,40 @@ describe("validator", () => { }); }); + test("parses JSON content types case-insensitively", async () => { + const app = buildApp(); + + const response = await app.request("/echo", { + method: "POST", + headers: { "content-type": "Application/JSON; Charset=UTF-8" }, + body: JSON.stringify({ name: "hello", nested: { count: 1 } }), + }); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual({ + ok: true, + echo: { name: "hello", nested: { count: 1 } }, + }); + }); + + test("parses JSON suffix content types with parameter whitespace", async () => { + const app = buildApp(); + + const response = await app.request("/echo", { + method: "POST", + headers: { + "content-type": "Application/VND.OPENIAP+JSON ; Charset=UTF-8", + }, + body: JSON.stringify({ name: "hello", nested: { count: 1 } }), + }); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual({ + ok: true, + echo: { name: "hello", nested: { count: 1 } }, + }); + }); + test("returns 400 with INVALID_INPUT errors for invalid payloads", async () => { const app = buildApp(); @@ -64,4 +98,44 @@ describe("validator", () => { expect(paths).toContain("name"); expect(paths).toContain("nested.count"); }); + + test("returns 413 before validation for oversized JSON bodies", async () => { + const app = buildApp(); + + const response = await app.request("/echo", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + name: "hello", + nested: { count: 1 }, + padding: "x".repeat(32 * 1024), + }), + }); + + expect(response.status).toBe(413); + await expect(response.json()).resolves.toEqual({ + errors: [ + { code: "PAYLOAD_TOO_LARGE", message: "Request body is too large" }, + ], + }); + }); + + test("returns 413 before reading oversized content-length", async () => { + const app = buildApp(); + + const response = await app.request("/echo", { + method: "POST", + headers: { + "content-type": "application/json", + "content-length": String(32 * 1024 + 1), + }, + }); + + expect(response.status).toBe(413); + await expect(response.json()).resolves.toEqual({ + errors: [ + { code: "PAYLOAD_TOO_LARGE", message: "Request body is too large" }, + ], + }); + }); }); diff --git a/packages/kit/server/api/v1/validator.ts b/packages/kit/server/api/v1/validator.ts index 30ca97b5..754f4a66 100644 --- a/packages/kit/server/api/v1/validator.ts +++ b/packages/kit/server/api/v1/validator.ts @@ -1,47 +1,125 @@ -import { validator as honoValidator } from "hono-openapi"; - -// Hono-openapi's Hook callback signature gets re-derived from a generic -// chain that resolves slightly differently depending on which hoisted -// copy of hono-openapi tsc picks up (Bun installs multiple peer-dep -// variants under node_modules/.bun). Cast to a stable narrow shape so -// the file typechecks the same on every install layout — without this, -// tsc reports `result` and `c` as implicit `any` on a fresh install. +import type { Context, MiddlewareHandler } from "hono"; +import { resolver, uniqueSymbol } from "hono-openapi"; + +import { + isContentLengthOverLimit, + JsonBodyTooLargeError, + readJsonBodyWithLimit, +} from "./request-body"; + +// Keep this JSON validator local instead of delegating to +// hono-openapi's validator: Hono's built-in JSON parser reads the +// whole body before schema validation, while verify requests need an +// edge cap before parsing. We still attach hono-openapi's metadata so +// generated OpenAPI request schemas keep working. type ValidatorIssue = { message: string; path?: ReadonlyArray }; -type ValidatorResult = +type ValidatorSchema = Parameters[0]; +type ValidatorResult = | { success: true; data: unknown } - | { success: false; error: ReadonlyArray; data: unknown }; + | { success: false; error: ReadonlyArray; data: unknown } + | { issues: ReadonlyArray } + | { value: Output }; -export function validator[1]>( - schema: Schema, -) { - return honoValidator( - "json", - schema, - ( - result: ValidatorResult, - // Hono context is typed as `any`-generic here intentionally — see - // comment above. We use only `c.json(...)`, which is stable. - c: { json: (body: unknown, status: number) => Response }, - ) => { - if (result.success) { - return; +const MAX_VALIDATOR_JSON_BODY_BYTES = 32 * 1024; + +export function validator(schema: Schema) { + const middleware: MiddlewareHandler = async (c, next) => { + let value: unknown = {}; + const contentType = c.req.header("content-type"); + if (isJsonContentType(contentType)) { + if ( + isContentLengthOverLimit( + c.req.header("content-length"), + MAX_VALIDATOR_JSON_BODY_BYTES, + ) + ) { + return payloadTooLarge(c, "Request body is too large"); + } + try { + value = await readJsonBodyWithLimit( + c.req.raw, + MAX_VALIDATOR_JSON_BODY_BYTES, + "Request body is too large", + ); + } catch (error) { + if (error instanceof JsonBodyTooLargeError) { + return payloadTooLarge(c, error.message); + } + return c.json( + { errors: [{ code: "INVALID_INPUT", message: "Body is not JSON" }] }, + 400, + ); } + } - const errors = []; + const result = (await schema["~standard"].validate( + value, + )) as ValidatorResult; + if ("issues" in result && result.issues) { + return validationError(c, value, result.issues); + } + if ("success" in result && result.success === false) { + return validationError(c, result.data, result.error); + } - for (const issue of result.error) { - errors.push({ - code: "INVALID_INPUT", - message: issue.message, - path: issuePathToString(issue.path), - }); - } + const data = + "success" in result && result.success === true + ? result.data + : "value" in result + ? result.value + : value; + c.req.addValidatedData("json", data as Record); + return next(); + }; + + return Object.assign(middleware, { + [uniqueSymbol]: { + target: "json", + ...resolver(schema), + }, + }); +} - return c.json({ errors }, 400); +function payloadTooLarge(c: Context, message: string) { + return c.json( + { + errors: [{ code: "PAYLOAD_TOO_LARGE", message }], }, + 413, + ); +} + +function isJsonContentType(contentType: string | undefined): boolean { + if (!contentType) { + return false; + } + const mediaType = contentType.split(";")[0]?.trim().toLowerCase(); + return ( + mediaType === "application/json" || + Boolean( + mediaType?.startsWith("application/") && mediaType.endsWith("+json"), + ) ); } +function validationError( + c: Context, + _data: unknown, + issues: ReadonlyArray, +) { + const errors = []; + + for (const issue of issues) { + errors.push({ + code: "INVALID_INPUT", + message: issue.message, + path: issuePathToString(issue.path), + }); + } + + return c.json({ errors }, 400); +} + function issuePathToString( path: ReadonlyArray | undefined, ): string | undefined { diff --git a/packages/kit/server/api/v1/webhooks.test.ts b/packages/kit/server/api/v1/webhooks.test.ts index 3548eef3..f4ca7793 100644 --- a/packages/kit/server/api/v1/webhooks.test.ts +++ b/packages/kit/server/api/v1/webhooks.test.ts @@ -1,4 +1,5 @@ import { beforeAll, describe, expect, it } from "vitest"; +import { Hono } from "hono"; let helpers: typeof import("./webhooks"); @@ -31,6 +32,102 @@ describe("pubSubOidcAudiences", () => { }); }); +describe("webhooksRoutes", () => { + it("rejects oversized path apiKey before reading the body", async () => { + const app = new Hono(); + app.route("/webhooks", helpers.webhooksRoutes); + + const response = await app.request(`/webhooks/${"a".repeat(129)}`, { + method: "POST", + }); + + expect(response.status).toBe(403); + await expect(response.json()).resolves.toEqual({ + errors: [{ code: "INVALID_API_KEY", message: "API key is too long" }], + }); + }); + + it("rejects blank path apiKey before reading the body", async () => { + const app = new Hono(); + app.route("/webhooks", helpers.webhooksRoutes); + + const response = await app.request("/webhooks/%20%20", { + method: "POST", + }); + + expect(response.status).toBe(403); + await expect(response.json()).resolves.toEqual({ + errors: [{ code: "INVALID_API_KEY", message: "API key is required" }], + }); + }); + + it("rejects oversized webhook bodies before JSON parsing", async () => { + const app = new Hono(); + app.route("/webhooks", helpers.webhooksRoutes); + + const response = await app.request("/webhooks/openiap-kit_secret", { + method: "POST", + headers: { "content-length": String(256 * 1024 + 1) }, + }); + + expect(response.status).toBe(413); + await expect(response.json()).resolves.toEqual({ + errors: [ + { code: "PAYLOAD_TOO_LARGE", message: "Webhook payload is too large" }, + ], + }); + }); +}); + +describe("legacyUnsupportedEventReason", () => { + it("keeps legacy unsupported-event responses free of raw error details", () => { + expect( + helpers.legacyUnsupportedEventReason( + new Error("UNSUPPORTED_EVENT: raw payload details"), + ), + ).toBe("Unsupported event"); + expect(helpers.legacyUnsupportedEventReason(new Error("OTHER"))).toBeNull(); + }); +}); + +describe("isWebhookBodyTooLarge", () => { + it("only rejects declared webhook bodies over the cap", () => { + expect(helpers.isWebhookBodyTooLarge(undefined)).toBe(false); + expect(helpers.isWebhookBodyTooLarge(String(256 * 1024))).toBe(false); + expect(helpers.isWebhookBodyTooLarge(String(256 * 1024 + 1))).toBe(true); + expect(helpers.isWebhookBodyTooLarge(String(Number.MAX_SAFE_INTEGER))).toBe( + true, + ); + expect(helpers.isWebhookBodyTooLarge("not-a-number")).toBe(false); + }); +}); + +describe("readWebhookJsonBody", () => { + it("rejects streamed webhook bodies over the cap", async () => { + const request = new Request("https://kit.openiap.dev/v1/webhooks/key", { + method: "POST", + body: JSON.stringify({ signedPayload: "a".repeat(256 * 1024) }), + }); + + await expect(helpers.readWebhookJsonBody(request)).rejects.toThrow( + "Webhook payload is too large", + ); + }); +}); + +describe("webhookStreamUnavailableError", () => { + it("does not expose raw stream lookup failures", () => { + expect(helpers.webhookStreamUnavailableError()).toEqual({ + errors: [ + { + code: "WEBHOOK_STREAM_UNAVAILABLE", + message: "Webhook stream is temporarily unavailable", + }, + ], + }); + }); +}); + describe("isAllowedPubSubServiceAccount", () => { it("accepts verified Google service account principals by default", () => { expect( @@ -62,12 +159,121 @@ describe("isAllowedPubSubServiceAccount", () => { }); }); -describe("sanitizePubSubAudienceForLog", () => { - it("redacts webhook api keys from audience logs", () => { +describe("extractBearerToken", () => { + it("accepts bearer scheme case-insensitively with flexible spacing", () => { + expect(helpers.extractBearerToken("Bearer jwt-token")).toBe("jwt-token"); + expect(helpers.extractBearerToken("bearer jwt-token")).toBe("jwt-token"); + expect(helpers.extractBearerToken(" BEARER jwt-token ")).toBe( + "jwt-token", + ); + }); + + it("rejects missing, non-bearer, or ambiguous authorization headers", () => { + expect(helpers.extractBearerToken(undefined)).toBeNull(); + expect(helpers.extractBearerToken("Basic abc")).toBeNull(); + expect(helpers.extractBearerToken("Bearer")).toBeNull(); + expect(helpers.extractBearerToken("Bearer token extra")).toBeNull(); + }); +}); + +describe("decodePubSubMessageData", () => { + it("decodes strict base64 JSON objects", () => { + const encoded = Buffer.from( + JSON.stringify({ packageName: "dev.hyo.app" }), + ).toString("base64"); + + expect(helpers.decodePubSubMessageData(encoded)).toEqual({ + decodedRaw: '{"packageName":"dev.hyo.app"}', + decoded: { packageName: "dev.hyo.app" }, + }); + }); + + it("rejects malformed base64 instead of letting Buffer ignore junk", () => { + const encoded = Buffer.from(JSON.stringify({ ok: true })).toString( + "base64", + ); + + expect(helpers.decodePubSubMessageData(`${encoded}!`)).toBeNull(); + expect(helpers.decodePubSubMessageData("not base64")).toBeNull(); + }); + + it("rejects decoded JSON primitives", () => { + const encoded = Buffer.from('"not-an-object"').toString("base64"); + + expect(helpers.decodePubSubMessageData(encoded)).toBeNull(); + }); +}); + +describe("resolveGoogleEventTimeMillis", () => { + it("accepts non-negative safe integer millis from Pub/Sub data", () => { expect( - helpers.sanitizePubSubAudienceForLog( - "https://kit.openiap.dev/v1/webhooks/openiap-kit_secret", + helpers.resolveGoogleEventTimeMillis("1700000000000", undefined), + ).toBe(1_700_000_000_000); + expect(helpers.resolveGoogleEventTimeMillis(1700000000000, undefined)).toBe( + 1_700_000_000_000, + ); + }); + + it("falls back to publishTime or now for malformed eventTimeMillis", () => { + const publishTime = "2024-01-02T03:04:05.000Z"; + const publishedAt = Date.parse(publishTime); + + expect(helpers.resolveGoogleEventTimeMillis("0x10", publishTime, 123)).toBe( + publishedAt, + ); + expect(helpers.resolveGoogleEventTimeMillis("1e3", undefined, 123)).toBe( + 123, + ); + expect( + helpers.resolveGoogleEventTimeMillis( + String(Number.MAX_SAFE_INTEGER + 1), + undefined, + 123, ), - ).toBe("https://kit.openiap.dev/v1/webhooks/"); + ).toBe(123); + }); +}); + +describe("sanitizePubSubAudienceForLog", () => { + it("preserves webhook endpoint audience logs", () => { + const cases = [ + [ + "https://kit.openiap.dev/v1/webhooks/openiap-kit_secret", + "https://kit.openiap.dev/v1/webhooks/openiap-kit_secret", + ], + [ + "https://kit.openiap.dev/v1/webhooks/apple/openiap-kit_secret", + "https://kit.openiap.dev/v1/webhooks/apple/openiap-kit_secret", + ], + [ + "https://kit.openiap.dev/v1/webhooks/google/openiap-kit_secret", + "https://kit.openiap.dev/v1/webhooks/google/openiap-kit_secret", + ], + [ + "https://kit.openiap.dev/v1/webhooks/stream/openiap-kit_secret?since=1", + "https://kit.openiap.dev/v1/webhooks/stream/openiap-kit_secret?since=1", + ], + [ + "https://kit.openiap.dev/v1/webhooks/openiap-kit_secret?apiKey=openiap-kit_query&token=jwt-token&id_token=id-token&jwt=jwt-token&since=1", + "https://kit.openiap.dev/v1/webhooks/openiap-kit_secret?apiKey=openiap-kit_query&token=jwt-token&id_token=id-token&jwt=jwt-token&since=1", + ], + [ + "https://kit.openiap.dev/api/v1/webhooks/openiap-kit_secret", + "https://kit.openiap.dev/api/v1/webhooks/openiap-kit_secret", + ], + ]; + + for (const [input, expected] of cases) { + expect(helpers.sanitizePubSubAudienceForLog(input)).toBe(expected); + } + }); +}); + +describe("normalizeLastEventId", () => { + it("drops oversized reconnect cursors before Convex lookup", () => { + expect(helpers.normalizeLastEventId(undefined)).toBeUndefined(); + expect(helpers.normalizeLastEventId("rtdn-msg-1")).toBe("rtdn-msg-1"); + expect(helpers.normalizeLastEventId("a".repeat(512))).toBe("a".repeat(512)); + expect(helpers.normalizeLastEventId("a".repeat(513))).toBeUndefined(); }); }); diff --git a/packages/kit/server/api/v1/webhooks.ts b/packages/kit/server/api/v1/webhooks.ts index 7589bf24..ada1cda2 100644 --- a/packages/kit/server/api/v1/webhooks.ts +++ b/packages/kit/server/api/v1/webhooks.ts @@ -1,11 +1,17 @@ import { Hono } from "hono"; -import type { Context } from "hono"; +import type { Context, Next } from "hono"; import { streamSSE } from "hono/streaming"; import { OAuth2Client } from "google-auth-library"; import { ConvexClient } from "convex/browser"; import { api } from "@/convex"; import { client, convexUrlForRealtime, handleConvexError } from "../../convex"; +import { apiKeyValidationError } from "./middleware"; +import { + isContentLengthOverLimit, + JsonBodyTooLargeError, + readJsonBodyWithLimit, +} from "./request-body"; import { drainWebhookEventBatches } from "./webhookStreamDrain"; // Shared reactive client for the SSE webhook stream. We keep a @@ -28,18 +34,56 @@ function getSharedReactiveClient(): ConvexClient { return sharedReactiveClient; } +function describeError(error: unknown): string { + return error instanceof Error ? error.name : typeof error; +} + +const MAX_WEBHOOK_BODY_BYTES = 256 * 1024; + +export function legacyUnsupportedEventReason(error: unknown): string | null { + const errorMessage = error instanceof Error ? error.message : String(error); + return errorMessage.startsWith("UNSUPPORTED_EVENT") + ? "Unsupported event" + : null; +} + +export function isWebhookBodyTooLarge(contentLengthHeader: string | undefined) { + return isContentLengthOverLimit(contentLengthHeader, MAX_WEBHOOK_BODY_BYTES); +} + +export function webhookStreamUnavailableError() { + return { + errors: [ + { + code: "WEBHOOK_STREAM_UNAVAILABLE", + message: "Webhook stream is temporarily unavailable", + }, + ], + }; +} + +export async function readWebhookJsonBody(request: Request): Promise { + return readJsonBodyWithLimit( + request, + MAX_WEBHOOK_BODY_BYTES, + "Webhook payload is too large", + ); +} + // Inbound webhook receivers for Apple ASN v2 and Google Pub/Sub RTDN. // // Auth model: // - Apple ASN does not support custom Authorization headers, so the // project's API key is encoded in the path: kit gives each project a // webhook URL of the form -// https://kit.openiap.dev/v1/webhooks/apple/{apiKey} -// to register in App Store Connect. The path segment behaves like a -// capability token; rotating the project's API key invalidates the -// URL just like it invalidates verifyReceipt callers. The Convex -// action verifies the signedPayload signature against Apple's roots, -// so even if the URL leaks, only Apple-signed payloads are accepted. +// https://kit.openiap.dev/v1/webhooks/{apiKey} +// to register in App Store Connect. Platform-specific /apple and +// /google aliases remain supported for existing store-console wiring. +// The path segment behaves like a capability token; rotating the +// project's API key invalidates the URL just like it invalidates +// verifyReceipt callers. The Convex action verifies the signedPayload +// signature against Apple's roots, so even if the URL leaks, only +// Apple-signed payloads are accepted. // // - Google Pub/Sub push delivers a Bearer JWT from Google in the // Authorization header that we verify against @@ -49,6 +93,11 @@ function getSharedReactiveClient(): ConvexClient { const webhooks = new Hono(); +webhooks.use("/:apiKey", pathApiKeyGuard); +webhooks.use("/apple/:apiKey", pathApiKeyGuard); +webhooks.use("/google/:apiKey", pathApiKeyGuard); +webhooks.use("/stream/:apiKey", pathApiKeyGuard); + // Unified lifecycle endpoint. The exact same URL works for both Apple // App Store Connect and Google Pub/Sub push subscriptions: kit // inspects the body shape to detect which store sent the @@ -83,8 +132,11 @@ const unifiedHandler = async (c: Context) => { } let body: unknown; try { - body = await c.req.json(); - } catch { + body = await readWebhookJsonBody(c.req.raw); + } catch (error) { + if (error instanceof JsonBodyTooLargeError) { + return webhookPayloadTooLargeResponse(c); + } return c.json( { errors: [{ code: "INVALID_INPUT", message: "Body is not JSON" }] }, 400, @@ -161,6 +213,37 @@ function looksLikeGoogle(body: unknown): boolean { return typeof m.data === "string" && typeof m.messageId === "string"; } +async function pathApiKeyGuard(c: Context, next: Next) { + const validationError = apiKeyValidationError(c.req.param("apiKey")); + if (validationError) { + return c.json( + { errors: [{ code: "INVALID_API_KEY", message: validationError }] }, + 403, + ); + } + if ( + c.req.method !== "GET" && + isWebhookBodyTooLarge(c.req.header("content-length")) + ) { + return webhookPayloadTooLargeResponse(c); + } + await next(); +} + +function webhookPayloadTooLargeResponse(c: Context) { + return c.json( + { + errors: [ + { + code: "PAYLOAD_TOO_LARGE", + message: "Webhook payload is too large", + }, + ], + }, + 413, + ); +} + async function handleAppleNotification( c: Context, apiKey: string, @@ -255,12 +338,8 @@ async function handleGoogleNotification( // signature verifiers see exactly what Google sent — JSON.stringify // would normalize spacing + key order and break any byte-level // verification). - let decodedRaw: string; - let decoded: Record; - try { - decodedRaw = Buffer.from(body.message.data, "base64").toString("utf-8"); - decoded = JSON.parse(decodedRaw); - } catch { + const decodedMessage = decodePubSubMessageData(body.message.data); + if (!decodedMessage) { return c.json( { errors: [ @@ -273,17 +352,16 @@ async function handleGoogleNotification( 400, ); } + const { decodedRaw, decoded } = decodedMessage; const payload = { messageId: body.message.messageId, packageName: typeof decoded.packageName === "string" ? decoded.packageName : undefined, - eventTimeMillis: - typeof decoded.eventTimeMillis === "string" - ? Number(decoded.eventTimeMillis) - : typeof decoded.eventTimeMillis === "number" - ? decoded.eventTimeMillis - : Date.parse(body.message.publishTime ?? "") || Date.now(), + eventTimeMillis: resolveGoogleEventTimeMillis( + decoded.eventTimeMillis, + body.message.publishTime, + ), subscriptionNotification: decoded.subscriptionNotification as | undefined | { @@ -353,28 +431,13 @@ async function handleGoogleNotification( // intermediate proxies (Fly edge, Cloudflare, browser fetch) don't // close the idle connection. const HEARTBEAT_MS = 25_000; - -// Drop fields the client doesn't need over the wire. `rawSignedPayload` -// holds the original JWS / Pub/Sub envelope including the upstream -// signature. Until kit grows per-purchaser SSE auth (tracked as -// follow-up — see PR #124 (https://github.com/hyodotdev/openiap/pull/124) review), the SSE feed is gated only by the -// project API key, so any holder of that key would otherwise see -// every other customer's signed payload. The client doesn't need it -// for normal reconciliation flows: `purchaseToken` + `productId` are -// enough to match against local state. Operators that DO need the -// raw payload can fetch it through an authenticated server-to-server -// query rather than a long-lived browser-readable stream. -function redactWebhookEventForStream( - event: Record, -): Record { - const { rawSignedPayload: _omit, ...rest } = event; - void _omit; - return rest; -} +const MAX_LAST_EVENT_ID_LENGTH = 512; webhooks.get("/stream/:apiKey", async (c) => { const apiKey = c.req.param("apiKey"); - const lastEventId = c.req.header("last-event-id") ?? undefined; + const lastEventId = normalizeLastEventId( + c.req.header("last-event-id") ?? undefined, + ); // Validate the API key BEFORE entering streamSSE. If the key is // wrong / rotated, every downstream `webhookEventsSince(apiKey, …)` @@ -383,9 +446,18 @@ webhooks.get("/stream/:apiKey", async (c) => { // never receive lifecycle updates after a key rotation. Returning a // 401 surfaces the misconfiguration immediately instead of looking // like a healthy idle stream. - const project = await client.query(api.projects.query.getProjectByApiKey, { - apiKey, - }); + let project: unknown; + try { + project = await client.query(api.projects.query.getProjectByApiKey, { + apiKey, + }); + } catch (error) { + console.error( + "[webhooks/stream] project lookup failed", + describeError(error), + ); + return c.json(webhookStreamUnavailableError(), 503); + } if (!project) { return c.json( { @@ -579,10 +651,13 @@ webhooks.get("/stream/:apiKey", async (c) => { id, event: typeof event.type === "string" ? event.type : "WebhookEvent", - data: JSON.stringify(redactWebhookEventForStream(event)), + data: JSON.stringify(event), }) .catch((err) => { - console.error("[webhooks/stream] drain write failed", err); + console.error( + "[webhooks/stream] drain write failed", + describeError(err), + ); }); if (typeof event.receivedAt === "number") { lastDeliveredReceivedAt = event.receivedAt; @@ -592,11 +667,11 @@ webhooks.get("/stream/:apiKey", async (c) => { if (batch.length < 500) break; } } catch (error) { - console.error("[webhooks/stream] drain failed", error); + console.error("[webhooks/stream] drain failed", describeError(error)); await stream.writeSSE({ event: "stream-error", data: JSON.stringify({ - message: error instanceof Error ? error.message : "Drain failed", + message: "Drain failed", }), }); // No reactive.close() — the client is shared across SSE @@ -656,10 +731,13 @@ webhooks.get("/stream/:apiKey", async (c) => { typeof event.type === "string" ? event.type : "WebhookEvent", - data: JSON.stringify(redactWebhookEventForStream(event)), + data: JSON.stringify(event), }) .catch((err) => { - console.error("[webhooks/stream] live write failed", err); + console.error( + "[webhooks/stream] live write failed", + describeError(err), + ); }); }, onIterationLimit: ({ iterations, cursor }) => { @@ -690,12 +768,14 @@ webhooks.get("/stream/:apiKey", async (c) => { liveCreationCursor = result.cursor.afterCreationTime; } while (liveDrainRequested && !aborted); } catch (error) { - console.error("[webhooks/stream] live drain failed", error); + console.error( + "[webhooks/stream] live drain failed", + describeError(error), + ); await stream.writeSSE({ event: "stream-error", data: JSON.stringify({ - message: - error instanceof Error ? error.message : "Live drain failed", + message: "Live drain failed", }), }); } finally { @@ -722,11 +802,11 @@ webhooks.get("/stream/:apiKey", async (c) => { }, ); } catch (error) { - console.error("[webhooks/stream] subscribe failed", error); + console.error("[webhooks/stream] subscribe failed", describeError(error)); await stream.writeSSE({ event: "stream-error", data: JSON.stringify({ - message: error instanceof Error ? error.message : "Subscribe failed", + message: "Subscribe failed", }), }); // unsubscribe() not needed — onUpdate threw before returning a @@ -807,25 +887,28 @@ async function resolveStreamStartCursor( return { sinceMs: Date.now() }; } catch (error) { const sanitized = - error instanceof Error - ? `${error.name}: ${error.message}` - : "(unknown error type)"; + error instanceof Error ? error.name : "(unknown error type)"; console.warn("[webhooks/stream] cursor resolution failed", sanitized); return { sinceMs: Date.now() }; } } +export function normalizeLastEventId(value: string | undefined) { + if (!value) return undefined; + return value.length <= MAX_LAST_EVENT_ID_LENGTH ? value : undefined; +} + const oauth2Client = new OAuth2Client(); async function verifyPubSubOidcToken( authHeader: string | undefined, audience: string | string[], ): Promise { - if (!authHeader?.startsWith("Bearer ")) { + const token = extractBearerToken(authHeader); + if (!token) { console.warn("[webhooks/google] OIDC verification failed: missing bearer"); return false; } - const token = authHeader.slice(7); const expectedAudiences = Array.isArray(audience) ? audience : [audience]; try { const ticket = await oauth2Client.verifyIdToken({ @@ -841,7 +924,7 @@ async function verifyPubSubOidcToken( if (!email || payload.email_verified !== true) { console.warn("[webhooks/google] OIDC verification failed: email", { audience: sanitizePubSubAudienceForLog(payload.aud), - email, + email: sanitizeEmailForLog(email), emailVerified: payload.email_verified, issuer: payload.iss, }); @@ -857,8 +940,10 @@ async function verifyPubSubOidcToken( if (!isAllowedPubSubServiceAccount(email, configuredPrincipal)) { console.warn("[webhooks/google] OIDC principal rejected", { audience: sanitizePubSubAudienceForLog(payload.aud), - configuredPrincipal: configuredPrincipal ?? "(any service account)", - email, + configuredPrincipal: configuredPrincipal + ? sanitizeEmailForLog(configuredPrincipal) + : "(any service account)", + email: sanitizeEmailForLog(email), expectedAudiences: expectedAudiences.map(sanitizePubSubAudienceForLog), issuer: payload.iss, }); @@ -867,18 +952,27 @@ async function verifyPubSubOidcToken( return true; } catch (error) { const sanitized = - error instanceof Error - ? `${error.name}: ${error.message}` - : "(unknown error type)"; + error instanceof Error ? error.name : "(unknown error type)"; console.warn("[webhooks/google] OIDC verification error", { error: sanitized, expectedAudiences: expectedAudiences.map(sanitizePubSubAudienceForLog), - tokenClaims: decodeJwtPayloadForLog(token), }); return false; } } +export function extractBearerToken( + authHeader: string | undefined, +): string | null { + if (!authHeader) return null; + + const parts = authHeader.trim().split(/\s+/); + if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer") { + return null; + } + return parts[1] || null; +} + export function isAllowedPubSubServiceAccount( email: string, configuredPrincipal?: string, @@ -887,6 +981,58 @@ export function isAllowedPubSubServiceAccount( return email.endsWith(".gserviceaccount.com"); } +export function decodePubSubMessageData( + data: string, +): { decodedRaw: string; decoded: Record } | null { + const trimmed = data.trim(); + if (!isStrictBase64(trimmed)) return null; + + try { + const decodedRaw = Buffer.from(trimmed, "base64").toString("utf-8"); + const decoded: unknown = JSON.parse(decodedRaw); + if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) { + return null; + } + return { decodedRaw, decoded: decoded as Record }; + } catch { + return null; + } +} + +export function resolveGoogleEventTimeMillis( + eventTimeMillis: unknown, + publishTime: string | undefined, + now = Date.now(), +): number { + const parsedEventTime = parseGoogleMillis(eventTimeMillis); + if (parsedEventTime !== undefined) return parsedEventTime; + + const parsedPublishTime = Date.parse(publishTime ?? ""); + return Number.isFinite(parsedPublishTime) ? parsedPublishTime : now; +} + +function parseGoogleMillis(value: unknown): number | undefined { + if (typeof value === "number") { + return Number.isSafeInteger(value) && value >= 0 ? value : undefined; + } + if (typeof value !== "string") return undefined; + + const trimmed = value.trim(); + if (!/^\d+$/.test(trimmed)) return undefined; + const parsed = Number(trimmed); + return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : undefined; +} + +function isStrictBase64(value: string): boolean { + if (value.length === 0 || value.length % 4 === 1) return false; + if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return false; + + const firstPadding = value.indexOf("="); + if (firstPadding === -1) return true; + + return value.length % 4 === 0 && /^=+$/.test(value.slice(firstPadding)); +} + export function pubSubOidcAudiences( requestUrl: string, configuredAudience: string, @@ -922,32 +1068,6 @@ function safeUrl(value: string): URL | null { } } -type JwtClaimsForLog = { - aud?: string | string[]; - email?: string; - emailVerified?: boolean; - issuer?: string; -}; - -function decodeJwtPayloadForLog(token: string): JwtClaimsForLog | null { - const [, payload] = token.split("."); - if (!payload) return null; - try { - const decoded = JSON.parse(Buffer.from(payload, "base64url").toString()); - return { - aud: sanitizePubSubAudienceForLog(decoded.aud), - email: typeof decoded.email === "string" ? decoded.email : undefined, - emailVerified: - typeof decoded.email_verified === "boolean" - ? decoded.email_verified - : undefined, - issuer: typeof decoded.iss === "string" ? decoded.iss : undefined, - }; - } catch { - return null; - } -} - export function sanitizePubSubAudienceForLog( audience: unknown, ): string | string[] | undefined { @@ -959,11 +1079,12 @@ export function sanitizePubSubAudienceForLog( if (typeof audience !== "string") return undefined; const parsed = safeUrl(audience); if (!parsed) return audience; - const path = parsed.pathname.replace( - /^(\/v1\/webhooks\/)[^/]+$/, - "$1", - ); - return `${parsed.origin}${path}${parsed.search}`; + return `${parsed.origin}${parsed.pathname}${parsed.search}`; +} + +function sanitizeEmailForLog(email: unknown): string | undefined { + if (typeof email !== "string" || email.length === 0) return undefined; + return email; } function mapWebhookError( @@ -1002,24 +1123,24 @@ function mapWebhookError( return c.json({ errors: [convexError] }, 400); } - const errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.startsWith("UNSUPPORTED_EVENT")) { + const legacyUnsupportedReason = legacyUnsupportedEventReason(error); + if (legacyUnsupportedReason !== null) { // Legacy fallback — kept until all action paths migrate to the // ConvexError shape above. - return c.json({ ok: true, dropped: true, reason: errorMessage }); + return c.json({ + ok: true, + dropped: true, + reason: legacyUnsupportedReason, + }); } - console.error( - `[webhooks/${source}] unexpected error`, - errorMessage, - error instanceof Error ? error.stack : "", - ); + console.error(`[webhooks/${source}] unexpected error`, describeError(error)); return c.json( { errors: [ { code: "WEBHOOK_INTERNAL_ERROR", - message: errorMessage, + message: "Webhook processing failed", }, ], }, diff --git a/packages/kit/server/convex.test.ts b/packages/kit/server/convex.test.ts new file mode 100644 index 00000000..60b2b8d6 --- /dev/null +++ b/packages/kit/server/convex.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { ConvexError } from "convex/values"; + +process.env.VITE_KIT_CONVEX_URL ??= "https://placeholder.convex.cloud"; + +const { handleConvexError } = await import("./convex"); + +describe("handleConvexError", () => { + it("returns structured ConvexError payloads", () => { + expect( + handleConvexError( + new ConvexError({ + code: "INVALID_API_KEY", + message: "Invalid API key", + }), + ), + ).toEqual({ + code: "INVALID_API_KEY", + message: "Invalid API key", + }); + }); + + it("returns legacy JSON ConvexError payloads", () => { + expect( + handleConvexError( + new ConvexError( + JSON.stringify({ + error: "INVALID_API_KEY", + message: "Invalid API key", + }), + ), + ), + ).toEqual({ + code: "INVALID_API_KEY", + message: "Invalid API key", + }); + }); + + it("does not expose unstructured ConvexError strings", () => { + expect(handleConvexError(new ConvexError("internal backend detail"))).toBe( + null, + ); + }); +}); diff --git a/packages/kit/server/convex.ts b/packages/kit/server/convex.ts index 0482bc16..e62d4c7e 100644 --- a/packages/kit/server/convex.ts +++ b/packages/kit/server/convex.ts @@ -95,11 +95,8 @@ function getConvexError(error: ConvexError): ApiError | null { } // Unstructured error — the mutation/action threw - // `new ConvexError("some message")`. Return a generic mapping so the - // API surface responds with the original message + a stable code - // rather than a 500 / "UNKNOWN_ERROR". - return { - code: "CONVEX_ERROR", - message: error.data, - }; + // `new ConvexError("some message")`. Treat it as internal so public + // route layers use their generic 500 fallback instead of exposing + // arbitrary backend details as client-safe text. + return null; } diff --git a/packages/kit/server/utils/env.test.ts b/packages/kit/server/utils/env.test.ts index 5754017a..82af1240 100644 --- a/packages/kit/server/utils/env.test.ts +++ b/packages/kit/server/utils/env.test.ts @@ -10,6 +10,10 @@ describe("parsePositiveNumber", () => { test("falls back for NaN / Infinity / non-numeric", () => { expect(parsePositiveNumber("pineapple", 60, 1)).toBe(60); + expect(parsePositiveNumber("120ms", 60, 1)).toBe(60); + expect(parsePositiveNumber("0x10", 60, 1)).toBe(60); + expect(parsePositiveNumber("1e2", 60, 1)).toBe(60); + expect(parsePositiveNumber("+1", 60, 1)).toBe(60); expect(parsePositiveNumber("NaN", 60, 1)).toBe(60); expect(parsePositiveNumber("Infinity", 60, 1)).toBe(60); }); @@ -23,6 +27,7 @@ describe("parsePositiveNumber", () => { test("returns the parsed value when finite and at-or-above min", () => { expect(parsePositiveNumber("120", 60, 1)).toBe(120); + expect(parsePositiveNumber(" 120 ", 60, 1)).toBe(120); expect(parsePositiveNumber("1", 60, 1)).toBe(1); }); }); @@ -35,7 +40,8 @@ describe("parsePort", () => { test("falls back for non-numeric or fractional values", () => { expect(parsePort("banana", 3000)).toBe(3000); - expect(parsePort("8080.5", 3000)).toBe(8080); // parseInt truncates — still valid + expect(parsePort("3000abc", 3000)).toBe(3000); + expect(parsePort("8080.5", 3000)).toBe(3000); expect(parsePort("NaN", 3000)).toBe(3000); }); @@ -48,6 +54,7 @@ describe("parsePort", () => { test("returns the parsed value for 1..65535", () => { expect(parsePort("1", 3000)).toBe(1); + expect(parsePort(" 3000 ", 8080)).toBe(3000); expect(parsePort("3000", 3000)).toBe(3000); expect(parsePort("8080", 3000)).toBe(8080); expect(parsePort("65535", 3000)).toBe(65_535); diff --git a/packages/kit/server/utils/env.ts b/packages/kit/server/utils/env.ts index ae85bd8f..5c399e42 100644 --- a/packages/kit/server/utils/env.ts +++ b/packages/kit/server/utils/env.ts @@ -21,7 +21,9 @@ export function parsePositiveNumber( min: number, ): number { if (raw === undefined || raw === "") return fallback; - const n = Number(raw); + const value = raw.trim(); + if (!/^\d+(?:\.\d+)?$/.test(value)) return fallback; + const n = Number(value); if (!Number.isFinite(n) || n < min) return fallback; return n; } @@ -33,7 +35,9 @@ export function parsePositiveNumber( */ export function parsePort(raw: string | undefined, fallback: number): number { if (raw === undefined || raw === "") return fallback; - const n = Number.parseInt(raw, 10); + const value = raw.trim(); + if (!/^\d+$/.test(value)) return fallback; + const n = Number(value); if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1 || n > 65_535) { return fallback; } diff --git a/packages/kit/src/components/Footer.tsx b/packages/kit/src/components/Footer.tsx index 338e6b58..4d38161b 100644 --- a/packages/kit/src/components/Footer.tsx +++ b/packages/kit/src/components/Footer.tsx @@ -88,7 +88,7 @@ export default function Footer() {
      • setIsMobileMenuOpen(false)} diff --git a/packages/kit/src/pages/auth/organization/create.tsx b/packages/kit/src/pages/auth/organization/create.tsx index 0ef36799..26ca3759 100644 --- a/packages/kit/src/pages/auth/organization/create.tsx +++ b/packages/kit/src/pages/auth/organization/create.tsx @@ -131,7 +131,7 @@ export default function CreateOrganization() {
        - openiap-kit.com/ + kit.openiap.dev/ acc + (p.apiKey ? 1 : 0), 0) || 0, + value: projects?.reduce((acc, p) => acc + (p.hasApiKey ? 1 : 0), 0) || 0, icon: Key, color: "text-purple-500", bgColor: "bg-purple-500/10", diff --git a/packages/kit/src/pages/auth/organization/project/analytics.tsx b/packages/kit/src/pages/auth/organization/project/analytics.tsx index 9020fa5a..b359a088 100644 --- a/packages/kit/src/pages/auth/organization/project/analytics.tsx +++ b/packages/kit/src/pages/auth/organization/project/analytics.tsx @@ -30,7 +30,8 @@ import { api } from "@/convex"; import { PageLoading } from "@/components/LoadingSpinner"; import { cn, formatMicros, normalizeCurrencyCode } from "@/lib/utils"; -type ProjectContext = { project: Doc<"projects"> }; +type DashboardProject = Omit, "apiKey" | "horizonAppSecret">; +type ProjectContext = { project: DashboardProject }; type Platform = "IOS" | "Android"; type PlatformFilter = "all" | Platform; @@ -174,8 +175,8 @@ export default function ProjectAnalytics() { // off the dep list because `maxFromDay` / `toDay` are derived // from it via `utcDayKey(...)` and only change at UTC midnight. const queryArgs = useMemo( - () => ({ apiKey: project.apiKey, fromDay: maxFromDay, toDay }), - [project.apiKey, maxFromDay, toDay], + () => ({ projectId: project._id, fromDay: maxFromDay, toDay }), + [project._id, maxFromDay, toDay], ); const metrics = useQuery( api.subscriptions.query.getRevenueMetrics, diff --git a/packages/kit/src/pages/auth/organization/project/apikeys.tsx b/packages/kit/src/pages/auth/organization/project/apikeys.tsx index 75aaee23..81be7b61 100644 --- a/packages/kit/src/pages/auth/organization/project/apikeys.tsx +++ b/packages/kit/src/pages/auth/organization/project/apikeys.tsx @@ -6,7 +6,6 @@ import { toast } from "sonner"; import { Key, Plus, - Copy, Trash2, RefreshCw, Calendar, @@ -115,11 +114,6 @@ export default function ApiKeys() { } }; - const copyToClipboard = (key: string) => { - void navigator.clipboard.writeText(key); - toast.success("Copied to clipboard"); - }; - if (!project) { return ; } @@ -276,16 +270,8 @@ export default function ApiKeys() {
        - {apiKey.key} + {apiKey.keyPreview} -
        diff --git a/packages/kit/src/pages/auth/organization/project/productPrice.test.ts b/packages/kit/src/pages/auth/organization/project/productPrice.test.ts new file mode 100644 index 00000000..418aa769 --- /dev/null +++ b/packages/kit/src/pages/auth/organization/project/productPrice.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; + +import { usdPriceToMicros } from "./productPrice"; + +describe("usdPriceToMicros", () => { + it("converts decimal USD strings to micros", () => { + expect(usdPriceToMicros("9")).toBe(9_000_000); + expect(usdPriceToMicros("9.99")).toBe(9_990_000); + expect(usdPriceToMicros(" 0.000001 ")).toBe(1); + }); + + it("returns undefined for empty, zero, malformed, and unsafe prices", () => { + expect(usdPriceToMicros("")).toBeUndefined(); + expect(usdPriceToMicros("0")).toBeUndefined(); + expect(usdPriceToMicros("12abc")).toBeUndefined(); + expect(usdPriceToMicros("1e2")).toBeUndefined(); + expect(usdPriceToMicros("1.1234567")).toBeUndefined(); + expect(usdPriceToMicros(String(Number.MAX_SAFE_INTEGER))).toBeUndefined(); + }); +}); diff --git a/packages/kit/src/pages/auth/organization/project/productPrice.ts b/packages/kit/src/pages/auth/organization/project/productPrice.ts new file mode 100644 index 00000000..1ec4240f --- /dev/null +++ b/packages/kit/src/pages/auth/organization/project/productPrice.ts @@ -0,0 +1,15 @@ +export function usdPriceToMicros(raw: string): number | undefined { + const value = raw.trim(); + if (value === "") return undefined; + + const match = /^(\d+)(?:\.(\d{1,6}))?$/.exec(value); + if (!match) return undefined; + + const units = BigInt(match[1]); + const fraction = BigInt((match[2] ?? "").padEnd(6, "0")); + const micros = units * 1_000_000n + fraction; + if (micros <= 0n || micros > BigInt(Number.MAX_SAFE_INTEGER)) { + return undefined; + } + return Number(micros); +} diff --git a/packages/kit/src/pages/auth/organization/project/products.tsx b/packages/kit/src/pages/auth/organization/project/products.tsx index 7dba6a3a..d2e2f9ce 100644 --- a/packages/kit/src/pages/auth/organization/project/products.tsx +++ b/packages/kit/src/pages/auth/organization/project/products.tsx @@ -21,25 +21,27 @@ import { PageLoading } from "@/components/LoadingSpinner"; import { Modal } from "@/components/Modal"; import { Tooltip } from "@/components/Tooltip"; import { Badge, PlatformBadge } from "../../../../components/Badge"; +import { usdPriceToMicros } from "./productPrice"; -type ProjectContext = { project: Doc<"projects"> }; +type DashboardProject = Omit, "apiKey" | "horizonAppSecret">; +type ProjectContext = { project: DashboardProject }; type SyncJob = Doc<"productSyncJobs">; export default function ProjectProducts() { const { project } = useOutletContext(); const products = useQuery(api.products.query.listProducts, { - apiKey: project.apiKey, + projectId: project._id, }); const upsert = useMutation(api.products.mutation.upsertProduct); const enqueueSync = useMutation(api.products.jobs.enqueueProductSync); const cancelSync = useMutation(api.products.jobs.cancelProductSync); const dismissJob = useMutation(api.products.jobs.dismissCompletedJob); const iosJob = useQuery(api.products.jobs.getActiveSyncJob, { - apiKey: project.apiKey, + projectId: project._id, platform: "IOS", }); const androidJob = useQuery(api.products.jobs.getActiveSyncJob, { - apiKey: project.apiKey, + projectId: project._id, platform: "Android", }); const listAscGroups = useAction( @@ -197,20 +199,19 @@ export default function ProjectProducts() { // description / reviewNote. const description = draft.description.trim() || undefined; const reviewNote = draft.reviewNote.trim() || undefined; - const priceUsd = parseFloat(draft.priceUsd); - const priceAmountMicros = - Number.isFinite(priceUsd) && priceUsd > 0 - ? Math.round(priceUsd * 1_000_000) - : undefined; + const priceAmountMicros = usdPriceToMicros(draft.priceUsd); const isSubIos = draft.type === "Subscription" && draft.platform === "IOS"; - const subscriptionGroupName = - isSubIos && draft.subscriptionGroupName.trim() - ? draft.subscriptionGroupName.trim() - : undefined; + if (isSubIos && !draft.subscriptionGroupName.trim()) { + toast.error("Subscription group is required for iOS subscriptions"); + return; + } + const subscriptionGroupName = isSubIos + ? draft.subscriptionGroupName.trim() + : undefined; const billingPeriod = draft.type === "Subscription" ? draft.billingPeriod : undefined; await upsert({ - apiKey: project.apiKey, + projectId: project._id, productId: draft.productId, platform: draft.platform, type: draft.type, @@ -247,7 +248,7 @@ export default function ProjectProducts() { const dryRun = options?.dryRun === true; try { const { jobId, deduped } = await enqueueSync({ - apiKey: project.apiKey, + projectId: project._id, platform, direction: "both", ...(dryRun ? { dryRun: true } : {}), @@ -275,7 +276,7 @@ export default function ProjectProducts() { const label = platform === "IOS" ? "App Store Connect" : "Play Console"; try { const { jobId, deduped } = await enqueueSync({ - apiKey: project.apiKey, + projectId: project._id, platform, direction: "purge-local", }); @@ -295,7 +296,7 @@ export default function ProjectProducts() { const onCancel = async (jobId: SyncJob["_id"], label: string) => { try { - const result = await cancelSync({ apiKey: project.apiKey, jobId }); + const result = await cancelSync({ projectId: project._id, jobId }); // The mutation returns `{ ok: false, reason: "not active" }` // when the job already finished between render and click. // Showing "cancellation requested" in that case is misleading @@ -435,10 +436,11 @@ export default function ProjectProducts() { { if (ascGroupNames !== null || ascGroupLoadFailed) return; - void listAscGroups({ apiKey: project.apiKey }) + void listAscGroups({ projectId: project._id }) .then((groups) => setAscGroupNames(groups.map((g) => g.referenceName)), ) @@ -524,7 +526,7 @@ export default function ProjectProducts() { void onCancel(jobId, "App Store Connect"); }} onDismiss={(jobId) => { - void dismissJob({ apiKey: project.apiKey, jobId }); + void dismissJob({ projectId: project._id, jobId }); }} /> { - void dismissJob({ apiKey: project.apiKey, jobId }); + void dismissJob({ projectId: project._id, jobId }); }} />
        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 1b1bcc84..c66a4b92 100644 --- a/packages/kit/src/pages/auth/organization/project/purchase-detail.tsx +++ b/packages/kit/src/pages/auth/organization/project/purchase-detail.tsx @@ -37,6 +37,22 @@ type DetailItem = { monospace?: boolean; }; +function parseJson(value: string): unknown { + try { + return JSON.parse(value) as unknown; + } catch { + return value; + } +} + +function formatJson(value: unknown): string | null { + try { + return JSON.stringify(value, null, 2); + } catch { + return null; + } +} + function DetailSection({ title, items, @@ -187,26 +203,18 @@ export default function PurchaseDetail() { (purchase as { productId?: string | null }).productId ?? null; const remoteResponse = purchase.remoteResponse - ? (JSON.parse(purchase.remoteResponse) as Record) + ? parseJson(purchase.remoteResponse) : undefined; const formattedRemoteResponse: string | null = (() => { if (!purchase.remoteResponse) { return null; } - try { - return JSON.stringify(remoteResponse, null, 2); - } catch { - return purchase.remoteResponse; - } + return formatJson(remoteResponse); })(); const requestPayload = (() => { - try { - return JSON.stringify(purchase.requestData, null, 2); - } catch { - return null; - } + return formatJson(purchase.requestData); })(); const requestItems: DetailItem[] = [ diff --git a/packages/kit/src/pages/auth/organization/project/settings.tsx b/packages/kit/src/pages/auth/organization/project/settings.tsx index 35828f74..d00111ba 100644 --- a/packages/kit/src/pages/auth/organization/project/settings.tsx +++ b/packages/kit/src/pages/auth/organization/project/settings.tsx @@ -33,7 +33,6 @@ interface ProjectData { organizationId: Id<"organizations">; name: string; slug: string; - apiKey: string; platform?: string; androidPackageName?: string; iosBundleId?: string; @@ -45,7 +44,7 @@ interface ProjectData { horizonEnabled?: boolean; horizonAppId?: string | null; // The Meta App Secret is never returned by - // `api.projects.query.getProject` — the server redacts it so a + // `api.projects.query.getProject` — the server omits it so a // dashboard member can't exfiltrate the Horizon credential via the // browser (see convex/projects/query.ts). All we surface is // whether one is configured, so the UI can show "Configured / @@ -108,7 +107,7 @@ export default function ProjectSettings() { Boolean(project?.horizonEnabled), ); const [horizonAppId, setHorizonAppId] = useState(project?.horizonAppId ?? ""); - // Secret is ALWAYS empty on mount: the server redacts it from the + // Secret is ALWAYS empty on mount: the server omits it from the // query so we never receive it in the browser. `isReplacingHorizonAppSecret` // toggles between the "Configured ✓ / Replace" affordance and the // password input. Saving without replacing is a no-op for the @@ -360,7 +359,7 @@ export default function ProjectSettings() { // The secret input is only required when enabling Horizon from a // blank slate or when the user explicitly chose to replace it. // Otherwise an empty field means "leave the existing secret alone" - // — the server redacts it so we never had the original to compare. + // — the server omits it so we never had the original to compare. const horizonAppSecretNeeded = horizonEnabled && (!hasHorizonAppSecretConfigured || isReplacingHorizonAppSecret); @@ -457,7 +456,7 @@ export default function ProjectSettings() { // either first-time setup (no secret configured yet) or the // user clicked Replace. Existing-secret path (not replacing) // omits the field so the mutation leaves the stored secret - // alone (the query redacts it, so we never had it to + // alone (the query omits it, so we never had it to // round-trip anyway). Missing this on first-time setup used // to trip the server's "Enabling Meta Horizon requires a // Horizon App Secret" invariant. diff --git a/packages/kit/src/pages/auth/organization/project/subscriptions.tsx b/packages/kit/src/pages/auth/organization/project/subscriptions.tsx index 7d9091e3..a3ce095b 100644 --- a/packages/kit/src/pages/auth/organization/project/subscriptions.tsx +++ b/packages/kit/src/pages/auth/organization/project/subscriptions.tsx @@ -21,7 +21,8 @@ import { normalizeCurrencyCode, } from "@/lib/utils"; -type ProjectContext = { project: Doc<"projects"> }; +type DashboardProject = Omit, "apiKey" | "horizonAppSecret">; +type ProjectContext = { project: DashboardProject }; const STATE_FILTERS = [ { id: "all", label: "All" }, @@ -41,10 +42,10 @@ export default function ProjectSubscriptions() { const [filter, setFilter] = useState("all"); const metrics = useQuery(api.subscriptions.query.metricsSummary, { - apiKey: project.apiKey, + projectId: project._id, }); const subscriptions = useQuery(api.subscriptions.query.listSubscriptions, { - apiKey: project.apiKey, + projectId: project._id, state: filter === "all" ? undefined : filter, limit: 200, }); diff --git a/packages/kit/src/pages/auth/organization/project/webhooks.tsx b/packages/kit/src/pages/auth/organization/project/webhooks.tsx index b974aed2..ab097df4 100644 --- a/packages/kit/src/pages/auth/organization/project/webhooks.tsx +++ b/packages/kit/src/pages/auth/organization/project/webhooks.tsx @@ -12,8 +12,11 @@ import { import type { Doc } from "@/convex"; import { api } from "@/convex"; +import { PageLoading } from "@/components/LoadingSpinner"; -type ProjectContext = { project: Doc<"projects"> }; +type ProjectContext = { + project: Omit, "apiKey" | "horizonAppSecret">; +}; export default function ProjectWebhooks() { const { project } = useOutletContext(); @@ -27,15 +30,24 @@ export default function ProjectWebhooks() { : null; const baseUrl = window.location.origin; const setup = useQuery(api.projects.setupStatus.getSetupStatus, { - apiKey: project.apiKey, + projectId: project._id, }); + const endpointPaths = useQuery(api.projects.query.getWebhookEndpointPaths, { + projectId: project._id, + }); + + if (endpointPaths === undefined) { + return ; + } - const urls = { - unified: `${baseUrl}/v1/webhooks/${encodeURIComponent(project.apiKey)}`, - apple: `${baseUrl}/v1/webhooks/apple/${encodeURIComponent(project.apiKey)}`, - google: `${baseUrl}/v1/webhooks/google/${encodeURIComponent(project.apiKey)}`, - stream: `${baseUrl}/v1/webhooks/stream/${encodeURIComponent(project.apiKey)}`, - }; + const urls = endpointPaths + ? { + unified: `${baseUrl}${endpointPaths.unified}`, + apple: `${baseUrl}${endpointPaths.apple}`, + google: `${baseUrl}${endpointPaths.google}`, + stream: `${baseUrl}${endpointPaths.stream}`, + } + : null; return (
        @@ -80,109 +92,126 @@ export default function ProjectWebhooks() {
        ) : null} - - Paste this URL into both: -
          -
        • - App Store Connect → Apps → Your App → App Information → App - Store Server Notifications (Production + Sandbox). -
        • -
        • - Google Cloud Pub/Sub → Subscription → Push endpoint (then point - Play Console → Monetization setup → RTDN at the topic). -
        • -
        - - kit auto-detects the payload shape and dispatches to the right - verifier — Apple notifications signed with your{" "} - .p8 + Google Pub/Sub messages - with OIDC bearer. - - - POST-only — opening this URL in a browser returns 404 (that's - expected). Verify wiring with the curl recipe below or with App - Store Connect's "Send Test Notification" button.{" "} -
        - Full setup guide - - . - - - } - url={urls.unified} - external="https://developer.apple.com/documentation/appstoreservernotifications" - /> + {urls ? ( + + Paste this URL into both: +
          +
        • + App Store Connect → Apps → Your App → App Information → App + Store Server Notifications (Production + Sandbox). +
        • +
        • + Google Cloud Pub/Sub → Subscription → Push endpoint (then + point Play Console → Monetization setup → RTDN at the topic). +
        • +
        + + kit auto-detects the payload shape and dispatches to the right + verifier — Apple notifications signed with your{" "} + .p8 + Google Pub/Sub messages + with OIDC bearer. + + + POST-only — opening this URL in a browser returns 404 (that's + expected). Verify production wiring with App Store Connect's + "Send Test Notification" or Google Pub/Sub's authenticated push + delivery.{" "} + + Full setup guide + + . + + + } + url={urls.unified} + external="https://developer.apple.com/documentation/appstoreservernotifications" + /> + ) : ( +
        +
        Webhook endpoints unavailable
        +

        + Create or activate an API key, or ask an admin to view webhook + endpoints. +

        +
        + )} - - Open this URL with EventSource (or kit's per-SDK helper) to receive - normalized webhook events. Reconnects are handled automatically - using Last-Event-ID so events fired - during a closed connection are delivered in order on the next - connect. - - Long-lived text/event-stream{" "} - response — opening it in a browser shows a blank tab (expected). - Test it with{" "} - curl -N {urls.stream} or wire one - of the per-SDK hooks at{" "} - - openiap.dev/docs/webhooks - - . - - - } - url={urls.stream} - /> + {urls ? ( + + Open this URL with EventSource (or kit's per-SDK helper) to + receive normalized webhook events. Reconnects are handled + automatically using Last-Event-ID{" "} + so events fired during a closed connection are delivered in order + on the next connect. + + Long-lived text/event-stream{" "} + response — opening it in a browser shows a blank tab (expected). + Test it with{" "} + curl -N {urls.stream} or wire + one of the per-SDK hooks at{" "} + + openiap.dev/docs/webhooks + + . + + + } + url={urls.stream} + /> + ) : null} + + {urls ? ( +
        + + Advanced — platform-specific URLs (legacy) + +
        +

        + These URLs accept only the matching platform's payload. Use the + unified URL above unless an upstream tool insists on a + store-prefixed path. +

        + + +
        +
        + ) : null} -
        - - Advanced — platform-specific URLs (legacy) - -
        + {urls ? ( +
        +
        Local/dev receiver smoke test

        - These URLs accept only the matching platform's payload. Use the - unified URL above unless an upstream tool insists on a - store-prefixed path. + POST a synthetic Pub/Sub test message to the unified URL only on + local/dev deployments with{" "} + KIT_ALLOW_UNAUTHENTICATED_PUBSUB=1. + Hosted production Google RTDN requires Pub/Sub OIDC; use the + store-console test notification buttons there.

        - - -
        -
        - -
        -
        Live test
        -

        - POST a synthetic Pub/Sub test message to the unified URL to verify - wiring without going through the App Store / Play Console. The MCP - server's openiap_simulate_webhook{" "} - tool runs this same request. -

        -
        {`curl -X POST \\
        +          
        {`curl -X POST \\
           ${urls.unified} \\
           -H 'content-type: application/json' \\
           -d '{
        @@ -198,7 +227,8 @@ export default function ProjectWebhooks() {
               "publishTime": "${new Date().toISOString()}"
             }
           }'`}
        -
        +
    + ) : null} ); } diff --git a/packages/kit/src/pages/auth/organization/projects/ProjectCard.tsx b/packages/kit/src/pages/auth/organization/projects/ProjectCard.tsx index 87708643..e59bb9e0 100644 --- a/packages/kit/src/pages/auth/organization/projects/ProjectCard.tsx +++ b/packages/kit/src/pages/auth/organization/projects/ProjectCard.tsx @@ -9,7 +9,6 @@ interface ProjectCardProps { _id: Id<"projects">; name: string; slug: string; - apiKey: string; platform?: string; createdAt: number; }; diff --git a/packages/kit/src/pages/auth/organization/usage.tsx b/packages/kit/src/pages/auth/organization/usage.tsx index 0fe37788..a4adf079 100644 --- a/packages/kit/src/pages/auth/organization/usage.tsx +++ b/packages/kit/src/pages/auth/organization/usage.tsx @@ -90,7 +90,7 @@ export default function OrganizationUsagePage() { }

    OpenIAP Sponsorship. Support OpenIAP at any tier ($25 / $100 / $300 / $500 / $1,000) via PayPal or GitHub Sponsors at{" "} @@ -293,7 +293,7 @@ export default function IapkitJoinsOpenIap() { Running IAPKit in production and depending on it? Please consider sponsoring OpenIAP at{" "} diff --git a/packages/kit/src/pages/blog/index.tsx b/packages/kit/src/pages/blog/index.tsx index 451360cb..0da6c9b2 100644 --- a/packages/kit/src/pages/blog/index.tsx +++ b/packages/kit/src/pages/blog/index.tsx @@ -18,7 +18,7 @@ export default function BlogIndex() { publisher: { "@type": "Organization", name: "OpenIAP", - url: "https://www.openiap.dev", + url: "https://openiap.dev", }, blogPost: POSTS.map((post) => ({ "@type": "BlogPosting", diff --git a/packages/kit/src/pages/docs/sections/analytics.tsx b/packages/kit/src/pages/docs/sections/analytics.tsx index 8d777f0a..b356cdc1 100644 --- a/packages/kit/src/pages/docs/sections/analytics.tsx +++ b/packages/kit/src/pages/docs/sections/analytics.tsx @@ -8,7 +8,7 @@ export default function AnalyticsPage() {

    The Analytics tab visualizes revenue and subscription @@ -21,7 +21,7 @@ export default function AnalyticsPage() {

    - The dashboard reads from a daily-rolled-up table populated from + The dashboard reads from a daily-bucketed table populated from ingested Apple App Store Server Notifications v2 and{" "} Google Play Real-time Developer Notifications (RTDN). Without webhooks, the Analytics tab will stay empty regardless of how @@ -30,9 +30,9 @@ export default function AnalyticsPage() {

    Open the project's Webhooks tab to copy your - IAPKit-hosted webhook URLs and register them with the App Store / Play - Console. Once notifications start arriving, the next cron tick (within - 24h) will populate this view. + IAPKit-hosted lifecycle webhook URL and register it with the App Store + / Play Console. Once notifications start arriving, the next cron tick + (within about 10 minutes) will populate this view.

    @@ -52,18 +52,19 @@ export default function AnalyticsPage() {
  • Open the project's Webhooks tab in the dashboard. - Copy the per-store URLs (one for Apple ASN v2, one for Google RTDN). + Copy the lifecycle webhook URL. The same endpoint accepts Apple ASN v2 + and Google RTDN payloads.
  • Apple: in App Store Connect →{" "} - App Store Server Notifications, paste the Apple webhook URL - and select Version 2 for both Production and Sandbox + App Store Server Notifications, paste the lifecycle webhook + URL and select Version 2 for both Production and Sandbox environments.
  • Google: in Play Console → Monetization setup , point Real-time Developer Notifications to a Pub/Sub topic that fans - out to the Google webhook URL (Pub/Sub push subscription with the + out to the lifecycle webhook URL (Pub/Sub push subscription with the IAPKit URL as endpoint).
  • @@ -72,8 +73,8 @@ export default function AnalyticsPage() { Webhooks tab's event log.
  • - Wait up to 24h for the next analytics rollup tick — or trigger it - manually if you have access to the Convex dashboard ( + Wait up to about 10 minutes for the next analytics rollup tick — or + trigger it manually if you have access to the Convex dashboard ( recomputeRevenueMetricsForProject).
  • @@ -82,7 +83,7 @@ export default function AnalyticsPage() {

    Analytics data lives in a separate revenueMetricsDaily{" "} table that the Analytics tab reads from directly — the dashboard never - scans the raw webhook event log on render. A daily cron walks each + scans the raw webhook event log on render. A 10-minute cron walks each project's recent webhookEvents and writes one row per{" "} (day, productId, currency, platform) bucket.

    diff --git a/packages/kit/src/pages/docs/sections/api.tsx b/packages/kit/src/pages/docs/sections/api.tsx index b0774212..315c9ac2 100644 --- a/packages/kit/src/pages/docs/sections/api.tsx +++ b/packages/kit/src/pages/docs/sections/api.tsx @@ -12,9 +12,10 @@ export default function ApiReferencePage() { description="POST /v1/purchase/verify — request shapes, responses, errors, headers." >

    - IAPKit exposes exactly one endpoint. Every interaction with the service - from your backend goes through it. The full OpenAPI spec is also served - at{" "} + IAPKit exposes one core purchase-verification endpoint for your backend: + POST /v1/purchase/verify. Webhooks, subscription state, + and product-catalog operations live on separate project-scoped surfaces. + The full OpenAPI spec is also served at{" "} Authentication

    Every request must include a Bearer API key:

    - {`Authorization: Bearer openiap-kit_`} + {`Authorization: Bearer openiap-kit_`}

    Missing header → 401 MISSING_API_KEY. Wrong scheme or @@ -82,12 +83,14 @@ export default function ApiReferencePage() { }`} - +

    - Every string field is validated server-side for non-empty + per-field - length bounds. Oversized payloads return{" "} - 400 INVALID_INPUT before IAPKit calls Apple / - Google / Meta, so malformed clients don't burn your upstream quota. + 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.

    @@ -224,6 +227,13 @@ export default function ApiReferencePage() { Malformed body / unknown store / input exceeds size cap. + + 413 + PAYLOAD_TOO_LARGE + + Request body exceeds the 32 KB edge cap. + + 401 MISSING_API_KEY @@ -238,9 +248,16 @@ export default function ApiReferencePage() { 429 - RATE_LIMITED + + RATE_LIMITED +
    + DUPLICATE_PAYLOAD +
    + REPEATED_FAILURE + - Per-key burst bucket empty; check Retry-After. + Per-key or per-payload guard rejected the request; check + Retry-After. diff --git a/packages/kit/src/pages/docs/sections/introduction.tsx b/packages/kit/src/pages/docs/sections/introduction.tsx index 5c369d67..a2773aef 100644 --- a/packages/kit/src/pages/docs/sections/introduction.tsx +++ b/packages/kit/src/pages/docs/sections/introduction.tsx @@ -67,7 +67,8 @@ export default function IntroductionPage() { One Fly.io machine serves the dashboard, REST API, and the SPA under the same origin. Convex holds organizations, projects, API keys, store credentials, and persisted purchase rows; the Bun server front-ends{" "} - /api/v1/* and the static dashboard build. + /v1/* plus the /api/v1/* compatibility alias + and the static dashboard build.

             {`  your backend / device          IAPKit                         upstream store
    @@ -103,8 +104,8 @@ export default function IntroductionPage() {
               
                 API reference
               {" "}
    -          — the one endpoint you call from your server, with every request shape
    -          and error code.
    +          — the purchase-verification endpoint you call from your server, with
    +          every request shape and error code.
             
             
  • diff --git a/packages/kit/src/pages/docs/sections/operations.tsx b/packages/kit/src/pages/docs/sections/operations.tsx index 587899a9..20a19d92 100644 --- a/packages/kit/src/pages/docs/sections/operations.tsx +++ b/packages/kit/src/pages/docs/sections/operations.tsx @@ -13,17 +13,25 @@ export default function OperationsPage() {

    /v1/purchase/verify is protected by an in-memory token-bucket keyed on a SHA-256 hash of the API key. Defaults: - 60-request burst, 1 req/sec steady state — - equivalently 60 req/min sustained. Self-hosted deployments can tune via{" "} + 600-request burst, 10 req/sec steady state — + equivalently 600 req/min sustained. Self-hosted deployments can tune via{" "} RATE_LIMIT_CAPACITY and{" "} RATE_LIMIT_REFILL_PER_SEC.

    When the bucket empties, IAPKit returns 429 RATE_LIMITED{" "} - with a Retry-After header (seconds). Every response — - successful or otherwise — also carries X-RateLimit-Limit{" "} - and X-RateLimit-Remaining so your client can back off - before getting 429'd. + with a Retry-After header (seconds). The verify endpoint + also has a per-(API key, payload) replay guard; it returns{" "} + 429 DUPLICATE_PAYLOAD when the same receipt is retried too + aggressively, or 429 REPEATED_FAILURE during the short + cooldown after the upstream store rejects that exact payload. +

    +

    + Authenticated responses after the auth layer — successful responses, + validation errors, and 429s — carry X-RateLimit-Limit and{" "} + X-RateLimit-Remaining so your client can back off before + getting 429'd. 401 / 403 auth failures return before those headers are + attached.

    @@ -36,22 +44,26 @@ export default function OperationsPage() {

    Correlation IDs

    - Every response from the verify endpoint carries an{" "} + Every verify response after the auth-header shape check carries an{" "} X-Correlation-Id header — a UUIDv4 IAPKit generates at the - middleware level. The same id appears in the structured log line for - that request, so support can pivot from a customer report straight to - the exact log entry. + logger middleware level. The same id appears in the structured log line + for that request, so support can pivot from a customer report straight + to the exact log entry. Missing or malformed Authorization headers + return before the logger runs.

    {`HTTP/1.1 200 OK Content-Type: application/json X-Correlation-Id: 6ebb9c9e-2e6e-4f9a-9bf2-4a6a9d5f9d20 -X-RateLimit-Limit: 60 -X-RateLimit-Remaining: 57`} +X-RateLimit-Limit: 600 +X-RateLimit-Remaining: 599`}

    Structured logs

    -

    Each verify request emits one JSON line to stdout:

    +

    + Each verify request that reaches the logger emits one JSON line to + stdout: +

    {`{ "level": "info", @@ -90,9 +102,9 @@ X-RateLimit-Remaining: 57`}

    The server installs SIGTERM and SIGINT{" "} handlers that call Bun.serve().stop() and drain in-flight{" "} - /api/v1/* requests before the process exits. Fly.io sends{" "} - SIGTERM before stopping a machine, so rolling deploys don't - cut off requests mid-verify. + /v1/* and /api/v1/* requests before the + process exits. Fly.io sends SIGTERM before stopping a + machine, so rolling deploys don't cut off requests mid-verify.

    Outbound retries

    @@ -117,11 +129,15 @@ X-RateLimit-Remaining: 57`}

    Input size limits

      +
    • receipt verification body ≤ 32 KB before JSON parsing
    • +
    • product management body ≤ 64 KB before JSON parsing
    • +
    • subscription user-binding body ≤ 8 KB before JSON parsing
    • +
    • webhook push body ≤ 256 KB before JSON parsing
    • jws ≤ 16 KB (Apple)
    • - purchaseToken ≤ 2 KB (Google) + purchaseToken ≤ 2 KB (Google / subscription binding)
    • userId ≤ 256 chars (Horizon) @@ -129,11 +145,14 @@ X-RateLimit-Remaining: 57`}
    • sku ≤ 256 chars (Horizon)
    • +
    • + productId ≤ 256 chars (catalog / subscriptions) +

    - Oversized requests return 400 INVALID_INPUT before IAPKit - calls the upstream store, so a misbehaving client can't burn your Apple - / Google / Meta quota. + Oversized fields return 400 INVALID_INPUT; oversized + request bodies return 413 PAYLOAD_TOO_LARGE. Invalid inputs + stop before upstream store calls or Convex mutations.

    ); diff --git a/packages/kit/src/pages/docs/sections/projects.tsx b/packages/kit/src/pages/docs/sections/projects.tsx index 48903068..d5768127 100644 --- a/packages/kit/src/pages/docs/sections/projects.tsx +++ b/packages/kit/src/pages/docs/sections/projects.tsx @@ -92,8 +92,9 @@ export default function ProjectsPage() {

    All keys are: