├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitattributes
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── config.yml
├── stale.yml
└── workflows
│ ├── ci-example-android.yml
│ ├── ci-example-ios.yml
│ ├── ci-ktlint-android.yml
│ ├── ci.yml
│ ├── code-review.yml
│ ├── deploy-documentation.yml
│ ├── publish-next.yml
│ └── publish-package.yml
├── .gitignore
├── .monolinterrc
├── .npmignore
├── .prettierignore
├── .prettierrc.js
├── .release-it.js
├── .swiftlint.yml
├── .vscode
└── settings.json
├── .watchmanconfig
├── .yarnrc
├── CHANGELOG.md
├── CONTRIBUTING.md
├── IapExample
├── .bundle
│ └── config
├── .eslintrc.js
├── .gitignore
├── .prettierrc.js
├── .watchmanconfig
├── Gemfile
├── Gemfile.lock
├── README.md
├── __tests__
│ └── App.test.tsx
├── android
│ ├── app
│ │ ├── amazon.sdktester.json
│ │ ├── build.gradle
│ │ ├── debug.keystore
│ │ ├── proguard-rules.pro
│ │ └── src
│ │ │ ├── debug
│ │ │ ├── AndroidManifest.xml
│ │ │ └── java
│ │ │ │ └── com
│ │ │ │ └── iapexample
│ │ │ │ └── ReactNativeFlipper.java
│ │ │ ├── main
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── java
│ │ │ │ └── com
│ │ │ │ │ └── iapexample
│ │ │ │ │ ├── MainActivity.java
│ │ │ │ │ └── MainApplication.java
│ │ │ └── res
│ │ │ │ ├── drawable
│ │ │ │ └── rn_edit_text_material.xml
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ │ └── values
│ │ │ │ ├── strings.xml
│ │ │ │ └── styles.xml
│ │ │ └── release
│ │ │ └── java
│ │ │ └── com
│ │ │ └── iapexample
│ │ │ └── ReactNativeFlipper.java
│ ├── build.gradle
│ ├── gradle.properties
│ ├── gradle
│ │ └── wrapper
│ │ │ ├── gradle-wrapper.jar
│ │ │ └── gradle-wrapper.properties
│ ├── gradlew
│ ├── gradlew.bat
│ └── settings.gradle
├── app.json
├── babel.config.js
├── index.tsx
├── ios
│ ├── .xcode.env
│ ├── Configuration.storekit
│ ├── IapExample.xcodeproj
│ │ ├── project.pbxproj
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── IapExample.xcscheme
│ ├── IapExample.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── IapExample
│ │ ├── AppDelegate.h
│ │ ├── AppDelegate.mm
│ │ ├── Images.xcassets
│ │ │ ├── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ ├── Info.plist
│ │ ├── LaunchScreen.storyboard
│ │ ├── PrivacyInfo.xcprivacy
│ │ └── main.m
│ ├── IapExampleTests
│ │ ├── IapExampleTests.m
│ │ └── Info.plist
│ ├── Podfile
│ └── Podfile.lock
├── jest.config.js
├── metro.config.js
├── package.json
├── react-native.config.js
├── src
│ ├── App.tsx
│ ├── components
│ │ ├── Box.tsx
│ │ ├── Button.tsx
│ │ ├── Heading.tsx
│ │ ├── Row.tsx
│ │ ├── State.tsx
│ │ └── index.ts
│ ├── navigators
│ │ ├── StackNavigator.tsx
│ │ └── index.ts
│ ├── screens
│ │ ├── AvailablePurchases.tsx
│ │ ├── ClassSetup.tsx
│ │ ├── Examples.tsx
│ │ ├── Products.tsx
│ │ ├── PurchaseHistory.tsx
│ │ ├── Subscriptions.tsx
│ │ └── index.ts
│ └── utils
│ │ ├── constants.ts
│ │ ├── index.ts
│ │ ├── logs.ts
│ │ ├── platform.ts
│ │ └── theme.ts
├── tsconfig.json
└── yarn.lock
├── LICENSE
├── README.md
├── RNIap.podspec
├── android
├── build.gradle
├── gradle.properties
└── src
│ ├── amazon
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── dooboolab
│ │ └── rniap
│ │ ├── EventSender.kt
│ │ ├── PurchasingServiceProxy.kt
│ │ ├── PurchasingServiceProxyAmazonImpl.kt
│ │ ├── RNIapActivityListener.kt
│ │ ├── RNIapAmazonListener.kt
│ │ ├── RNIapAmazonModule.kt
│ │ └── RNIapPackage.kt
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── dooboolab
│ │ └── rniap
│ │ ├── PromiseUtils.kt
│ │ └── PromiseUtlis.kt
│ ├── play
│ └── java
│ │ └── com
│ │ └── dooboolab
│ │ └── rniap
│ │ ├── PlayUtils.kt
│ │ ├── RNIapActivityListener.kt
│ │ ├── RNIapModule.kt
│ │ └── RNIapPackage.kt
│ ├── testAmazon
│ └── java
│ │ └── com
│ │ └── dooboolab
│ │ └── rniap
│ │ └── RNIapAmazonModuleTest.kt
│ └── testPlay
│ └── java
│ └── com
│ └── dooboolab
│ └── rniap
│ └── RNIapModuleTest.kt
├── app.plugin.js
├── babel.config.js
├── bob.config.js
├── docs
├── .gitignore
├── README.md
├── babel.config.js
├── docs
│ ├── api-reference
│ │ ├── _category_.json
│ │ ├── hooks.md
│ │ └── methods
│ │ │ ├── _category_.json
│ │ │ ├── amazon
│ │ │ ├── _category_.json
│ │ │ └── validate-receipt-amazon.mdx
│ │ │ ├── android
│ │ │ ├── _category_.json
│ │ │ ├── acknowledge-purchase-android.mdx
│ │ │ ├── deep-link-to-subscriptions-android.mdx
│ │ │ ├── flush-failed-purchases-cached-as-pending-android.mdx
│ │ │ └── validate-receipt-android.mdx
│ │ │ ├── ios
│ │ │ ├── _category_.json
│ │ │ ├── buy-promoted-product.mdx
│ │ │ ├── clear-products-ios.mdx
│ │ │ ├── clear-transaction-ios.mdx
│ │ │ ├── get-pending-purchases-ios.mdx
│ │ │ ├── get-promoted-product-ios.mdx
│ │ │ ├── present-code-redemption-sheet-ios.mdx
│ │ │ ├── request-purchase-with-offer.mdx
│ │ │ ├── request-purchase-with-quantity-ios.mdx
│ │ │ └── validate-receipt-ios.mdx
│ │ │ └── listeners
│ │ │ ├── _category_.json
│ │ │ ├── promoted-product-listener.mdx
│ │ │ ├── purchase-error-listener.mdx
│ │ │ └── purchase-updated-listener.mdx
│ ├── faq.mdx
│ ├── get-started.mdx
│ ├── guides
│ │ ├── _category_.json
│ │ ├── amazon-iap.mdx
│ │ ├── lifecycle.mdx
│ │ ├── purchases.mdx
│ │ ├── receipts.mdx
│ │ └── troubleshooting.mdx
│ ├── migrate_to_10.0.0.mdx
│ ├── migrate_to_11.0.0.mdx
│ ├── migrate_to_12.0.0.mdx
│ ├── old-to-remove-available-purchase.mdx
│ ├── old-to-remove-product.mdx
│ └── support-us.mdx
├── docusaurus.config.js
├── package.json
├── sidebars.js
├── src
│ ├── css
│ │ └── custom.css
│ ├── pages
│ │ └── index.js
│ └── uis
│ │ ├── AdFit.js
│ │ └── AdFitTopFixed.js
├── static
│ ├── .nojekyll
│ └── img
│ │ ├── favicon.ico
│ │ ├── logo.png
│ │ └── react-native-iapv3.svg
└── yarn.lock
├── ios
├── IapSerializationUtils.swift
├── IapTypes.swift
├── IapUtils.swift
├── LatestPromiseKeeper.swift
├── RNIapIos-Bridging-Header.h
├── RNIapIos.m
├── RNIapIos.swift
├── RNIapIos.xcodeproj
│ └── project.pbxproj
├── RNIapIosSk2.m
├── RNIapIosSk2.swift
└── ThreadSafe.swift
├── jest.config.js
├── package.json
├── plugin
├── __tests__
│ ├── fixtures
│ │ └── buildGradleFiles.ts
│ └── withIAP-test.ts
├── build
│ ├── withIAP.d.ts
│ └── withIAP.js
├── jest.config.js
├── src
│ └── withIAP.ts
└── tsconfig.json
├── scripts
└── bootstrap.js
├── src
├── __tests__
│ └── iap.test.ts
├── eventEmitter.ts
├── hooks
│ ├── index.ts
│ ├── useIAP.ts
│ └── withIAPContext.tsx
├── iap.ts
├── index.ts
├── internal
│ ├── enhancedFetch.ts
│ ├── fillProductsWithAdditionalData.ts
│ ├── index.ts
│ └── platform.ts
├── modules
│ ├── amazon.ts
│ ├── android.ts
│ ├── common.ts
│ ├── index.ts
│ ├── ios.ts
│ └── iosSk2.ts
├── purchaseError.ts
└── types
│ ├── amazon.ts
│ ├── android.ts
│ ├── apple.ts
│ ├── appleSk2.ts
│ └── index.ts
├── test
└── mocks
│ └── react-native-modules.js
├── tsconfig.build.json
├── tsconfig.json
├── typedoc.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 |
7 | [*.{js,json,yml}]
8 | charset = utf-8
9 | indent_style = space
10 | indent_size = 2
11 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.js
3 | *.d.ts
4 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: '@babel/eslint-parser',
4 | extends: ['@react-native-community', 'prettier'],
5 | plugins: ['@typescript-eslint', 'prettier', 'simple-import-sort', 'jest'],
6 | rules: {
7 | '@typescript-eslint/no-unused-vars': 'off',
8 | 'prettier/prettier': 'error',
9 | 'simple-import-sort/imports': [
10 | 'error',
11 | {
12 | groups: [
13 | ['^\\u0000'],
14 | ['^react', '^@?\\w'],
15 | ['^\\.\\.(?!/?$)', '^\\.\\./?$'],
16 | ['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
17 | ],
18 | },
19 | ],
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.pbxproj -text
2 | # specific for windows script files
3 | *.bat text eol=crlf
4 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | open_collective: react-native-iap
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐛 Bug Report
3 | about: Report an issue to help us improve react-native-iap
4 | title: ''
5 | labels: 'bug'
6 | assignees: ''
7 | ---
8 |
9 | > Please use the [Discussion board](https://github.com/hyochan/react-native-iap/discussions) if you want to get some help. Please use issues to report bugs.
10 |
11 | **Description**
12 |
13 |
14 |
15 | **Expected Behavior**
16 |
17 |
18 |
19 | **Screenshots**
20 |
21 |
22 |
23 | **Environment:**
24 |
25 | - react-native-iap:
26 | - react-native:
27 | - Platforms (iOS, Android, emulator, simulator, device):
28 |
29 | **To Reproduce**
30 | Steps to reproduce the behavior:
31 |
32 | 1. Go to '...'
33 | 2. Press '...'
34 | 3. Error '...' is shown
35 |
36 | ---
37 |
38 | [Optional] **Additional Context**
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Configuration for probot-stale - https://github.com/probot/stale
2 | # Number of days of inactivity before an issue becomes stale
3 | daysUntilStale: 90
4 | # Number of days of inactivity before a stale issue is closed
5 | daysUntilClose: 30
6 | # Issues with these labels will never be considered stale
7 | exemptLabels:
8 | - Good first issue
9 | - For Discussion
10 | - Core Team
11 | - "Help Wanted :octocat:"
12 | - ":warning:Regression"
13 | - ":clock1:PR Pending"
14 | # Label to use when marking an issue as stale
15 | staleLabel: Stale
16 | # Comment to post when marking an issue as stale. Set to `false` to disable
17 | markComment: >
18 | Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs.
19 | You may also label this issue as "For Discussion" or "Good first issue" and I will leave it open.
20 | Thank you for your contributions.
21 | # Comment to post when closing a stale issue. Set to `false` to disable
22 | closeComment: >
23 | Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please feel free to create a new issue with up-to-date information.
24 | only: issues
25 |
--------------------------------------------------------------------------------
/.github/workflows/ci-example-android.yml:
--------------------------------------------------------------------------------
1 | name: CI / Example Android
2 |
3 | on:
4 | push:
5 | branches: [main, next]
6 | paths:
7 | - 'src/**'
8 | - 'android/**'
9 | - 'IapExample/android/**'
10 | - .github/ci-example-android.yml
11 |
12 | pull_request:
13 | types: [opened, synchronize, reopened]
14 | paths:
15 | - 'src/**'
16 | - 'android/**'
17 | - 'IapExample/android/**'
18 | - .github/ci-example-android.yml
19 |
20 | jobs:
21 | build_android_example:
22 | runs-on: ubuntu-latest
23 |
24 | steps:
25 | - name: Checkout repository
26 | uses: actions/checkout@v4
27 |
28 | - uses: actions/setup-java@v4
29 | with:
30 | distribution: 'zulu'
31 | java-version: '17.x'
32 |
33 | - name: Setup Node.js
34 | uses: actions/setup-node@v4
35 | with:
36 | cache: 'yarn'
37 |
38 | - name: Install dependencies
39 | run: yarn install --immutable
40 |
41 | - name: Install dependencies for `IapExample/`
42 | run: yarn install --immutable
43 | working-directory: IapExample
44 |
45 | - name: Setup kernel for react native, increase watchers
46 | run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
47 |
48 | - name: Grant execute permission for gradlew in example app
49 | run: chmod +x ./gradlew
50 | working-directory: IapExample/android
51 |
52 | - name: Build with example app with Gradle
53 | run: ./gradlew build test
54 | working-directory: IapExample/android
55 |
56 | - name: Unit tests results
57 | uses: actions/upload-artifact@v4
58 | with:
59 | name: play-unit-tests-results
60 | path: IapExample/android/build/reports/tests/testPlayDebugUnitTest/index.html
61 |
--------------------------------------------------------------------------------
/.github/workflows/ci-example-ios.yml:
--------------------------------------------------------------------------------
1 | name: CI / Example iOS
2 |
3 | on:
4 | push:
5 | branches: [main, next]
6 | paths:
7 | - 'src/**'
8 | - 'ios/**'
9 | - 'IapExample/ios/**'
10 | - .github/ci-example-ios.yml
11 |
12 | pull_request:
13 | types: [opened, synchronize, reopened]
14 | paths:
15 | - 'src/**'
16 | - 'ios/**'
17 | - 'IapExample/ios/**'
18 | - .github/ci-example-ios.yml
19 |
20 | jobs:
21 | build_ios_example:
22 | runs-on: macos-15
23 | env:
24 | NO_FLIPPER: 1
25 | IOS_SIMULATOR: 'platform=iOS Simulator,name=iPhone 16'
26 |
27 | steps:
28 | - name: Checkout repository
29 | uses: actions/checkout@v4
30 |
31 | - name: Setup Node.js
32 | uses: actions/setup-node@v4
33 | with:
34 | cache: 'yarn'
35 |
36 | - name: Install dependencies
37 | run: yarn install --immutable
38 |
39 | - name: Install dependencies for `IapExample/`
40 | run: yarn install --immutable
41 | working-directory: IapExample
42 |
43 | - name: Restore buildcache
44 | uses: mikehardy/buildcache-action@v2
45 | continue-on-error: true
46 | with:
47 | cache_key: ${{ runner.os }}-buildcache-${{ hashFiles('**/Podfile.lock') }}-${{ hashFiles('**/Podfile')}}
48 |
49 | - name: Setup Ruby (bundle)
50 | uses: ruby/setup-ruby@v1
51 | with:
52 | working-directory: IapExample
53 | bundler-cache: true
54 | ruby-version: '2.7'
55 |
56 | - name: Install SwiftLint
57 | run: brew install swiftlint
58 |
59 | - name: SwiftLint
60 | run: yarn lint:swift
61 |
62 | - name: Verify no files have changed after auto-fix
63 | run: git diff --exit-code HEAD '*.swift'
64 |
65 | - name: Restore Pods cache
66 | uses: actions/cache@v4
67 | with:
68 | path: |
69 | IapExample/ios/Pods
70 | ~/Library/Caches/CocoaPods
71 | ~/.cocoapods
72 | key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock')}}-${{ hashFiles('**/Podfile')}}
73 | restore-keys: ${{ runner.os }}-pods-
74 |
75 | - name: Install Pods
76 | run: bundle exec pod install
77 | working-directory: IapExample/ios
78 |
79 | - name: Install xcpretty
80 | run: gem install xcpretty
81 | working-directory: IapExample/ios
82 |
83 | - name: Build App
84 | uses: sersoft-gmbh/xcodebuild-action@v3
85 | with:
86 | workspace: IapExample/ios/IapExample.xcworkspace
87 | scheme: IapExample
88 | sdk: iphonesimulator
89 | destination: ${{ env.IOS_SIMULATOR }}
90 | action: build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
91 |
--------------------------------------------------------------------------------
/.github/workflows/ci-ktlint-android.yml:
--------------------------------------------------------------------------------
1 | name: CI / Ktlint Android
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - .github/workflows/ci-ktlint-android.yml
7 | - 'android/src/**/*.kt'
8 | - '**.kts'
9 |
10 | jobs:
11 | ktlint:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - run: |
16 | curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.48.2/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/local/bin/
17 | - name: run ktlint
18 | run: |
19 | ktlint --reporter=checkstyle,output=build/ktlint-report.xml
20 | continue-on-error: true
21 | - uses: yutailang0119/action-ktlint@v3
22 | with:
23 | report-path: build/*.xml # Support glob patterns by https://www.npmjs.com/package/@actions/glob
24 | continue-on-error: false # If annotations contain error of severity, action-ktlint exit 1.
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main, next]
6 | paths:
7 | - 'docs/**'
8 | - 'src/**'
9 | - 'scripts/**'
10 | - 'test/**'
11 | - '*.md'
12 | - '*.json'
13 | - '*.js'
14 | - '*.lock'
15 | - 'IapExample/src/**'
16 | - 'IapExample/*.json'
17 | - 'IapExample/*.js'
18 | - 'IapExample/*.lock'
19 | - 'IapExample/*.tsx'
20 | - .github/**
21 |
22 | pull_request:
23 | types: [opened, synchronize, reopened]
24 | paths:
25 | - 'docs/**'
26 | - 'src/**'
27 | - 'scripts/**'
28 | - 'test/**'
29 | - '*.md'
30 | - '*.json'
31 | - '*.js'
32 | - '*.lock'
33 | - 'IapExample/src/**'
34 | - 'IapExample/*.json'
35 | - 'IapExample/*.js'
36 | - 'IapExample/*.lock'
37 | - 'IapExample/*.tsx'
38 | - .github/**
39 |
40 | jobs:
41 | build:
42 | runs-on: ubuntu-latest
43 |
44 | steps:
45 | - name: Checkout repository
46 | uses: actions/checkout@v3
47 |
48 | - name: Setup Node.js
49 | uses: actions/setup-node@v3
50 | with:
51 | cache: 'yarn'
52 |
53 | - name: Install reviewdog
54 | uses: reviewdog/action-setup@v1
55 |
56 | - name: Install dependencies
57 | run: yarn install --immutable
58 |
59 | - name: Install node_modules for `IapExample/`
60 | run: yarn install --immutable
61 | working-directory: IapExample
62 |
63 | - name: Run TypeScript
64 | run: |
65 | yarn lint:tsc | reviewdog -name="tsc" -efm="%f(%l,%c): error TS%n: %m" -reporter="github-pr-review" -filter-mode="nofilter" -fail-on-error -tee
66 | env:
67 | REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
68 |
69 | - name: Run lint scripts
70 | run: yarn lint:ci
71 |
72 |
--------------------------------------------------------------------------------
/.github/workflows/code-review.yml:
--------------------------------------------------------------------------------
1 | name: Code Review GPT
2 |
3 | on:
4 | pull_request:
5 | branches: [main]
6 |
7 | permissions:
8 | pull-requests: write
9 | contents: read
10 |
11 | jobs:
12 | run_code_review:
13 | if: github.repository_owner == github.actor
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v3
17 | with:
18 | fetch-depth: 0
19 |
20 | - name: Code Review GPT
21 | uses: mattzcarey/code-review-gpt@v0.1.8
22 | with:
23 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
24 | MODEL: 'gpt-4o'
25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-documentation.yml:
--------------------------------------------------------------------------------
1 | name: Deploy / Documentation
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | push:
7 | branches: [main]
8 | paths:
9 | - 'docs/**'
10 | - 'src/**'
11 |
12 | jobs:
13 | publish-documentation:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout repository
18 | uses: actions/checkout@v3
19 |
20 | - name: Setup Node.js
21 | uses: actions/setup-node@v3
22 | with:
23 | cache: 'yarn'
24 |
25 | - name: Install packages and generate docs
26 | run: |
27 | yarn install
28 | pushd ./IapExample
29 | yarn install
30 | popd
31 | yarn typedoc --plugin typedoc-plugin-markdown --out docs/docs/api --entryDocument '..' ./src || true
32 |
33 | - name: Install packages and build
34 | run: |
35 | pushd ./docs
36 | yarn
37 | yarn build
38 | popd
39 |
40 | - name: Deploy
41 | uses: peaceiris/actions-gh-pages@v3
42 | with:
43 | github_token: ${{ secrets.GITHUB_TOKEN }}
44 | publish_dir: ./docs/build
45 | publish_branch: gh-pages
46 | cname: react-native-iap.hyo.dev
47 |
--------------------------------------------------------------------------------
/.github/workflows/publish-next.yml:
--------------------------------------------------------------------------------
1 | name: Publish next package
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | jobs:
8 | deploy:
9 | if: github.event.release.prerelease
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout repository
14 | uses: actions/checkout@v3
15 |
16 | - name: Setup Node.js
17 | uses: actions/setup-node@v3
18 | with:
19 | cache: 'yarn'
20 | registry-url: 'https://registry.npmjs.org'
21 |
22 | - name: Remove example code
23 | run: rm -rf IapExample
24 |
25 | - name: Install dependencies
26 | run: yarn install --immutable
27 |
28 | - name: Build typescript
29 | run: yarn lint:ci
30 |
31 | - name: Verify no files have changed after auto-fix
32 | run: git diff -- ":(exclude)IapExample/*" --exit-code HEAD
33 |
34 | - run: npm publish --tag next
35 | env:
36 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
37 |
--------------------------------------------------------------------------------
/.github/workflows/publish-package.yml:
--------------------------------------------------------------------------------
1 | name: Publish main package
2 |
3 | on:
4 | workflow_dispatch:
5 | release:
6 | types: [created]
7 |
8 | jobs:
9 | deploy:
10 | if: '!github.event.release.prerelease'
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout repository
14 | uses: actions/checkout@v3
15 |
16 | - name: Setup Node.js
17 | uses: actions/setup-node@v3
18 | with:
19 | cache: 'yarn'
20 | registry-url: 'https://registry.npmjs.org'
21 |
22 | - name: Remove example code
23 | run: rm -rf IapExample
24 |
25 | - name: Install dependencies
26 | run: yarn install --immutable
27 |
28 | - name: Run lint scripts
29 | run: yarn lint:ci
30 |
31 | - name: Verify no files have changed after auto-fix
32 | run: git diff -- ":(exclude)IapExample/*" --exit-code HEAD
33 |
34 | - run: npm publish
35 | env:
36 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | .yarn*
6 | # XDE
7 | #
8 | .expo/
9 |
10 | # VSCode
11 | #
12 | jsconfig.json
13 |
14 | # Xcode
15 | #
16 | build/
17 | *.pbxuser
18 | !default.pbxuser
19 | *.mode1v3
20 | !default.mode1v3
21 | *.mode2v3
22 | !default.mode2v3
23 | *.perspectivev3
24 | !default.perspectivev3
25 | xcuserdata
26 | *.xccheckout
27 | *.moved-aside
28 | DerivedData
29 | *.hmap
30 | *.ipa
31 | *.xcuserstate
32 | project.xcworkspace
33 |
34 | # Android/IJ
35 | #
36 | .classpath
37 | .cxx
38 | .gradle
39 | .idea
40 | .project
41 | .settings
42 | local.properties
43 | android.iml
44 |
45 | # Cocoapods
46 | #
47 | IapExample/ios/Pods
48 |
49 | # Ruby
50 | #
51 | IapExample/vendor/
52 |
53 | # node.js
54 | #
55 | node_modules/
56 | npm-debug.log
57 | yarn-debug.log
58 | yarn-error.log
59 |
60 | # BUCK
61 | #
62 | buck-out/
63 | \.buckd/
64 | android/app/libs
65 | android/keystores/debug.keystore
66 |
67 | # Expo
68 | #
69 | .expo/*
70 |
71 | # generated by bob
72 | #
73 | lib/
74 |
75 | # yarn@2
76 | #
77 | .pnp.*
78 | **/.yarn/*
79 | !**/.yarn/patches
80 | !**/.yarn/plugins
81 | !**/.yarn/releases
82 | !**/.yarn/sdks
83 | !**/.yarn/versions
84 |
85 | # Docs
86 | docs/docs/api/*
87 |
88 | # Keep expo plugin build code
89 | !plugin/build
--------------------------------------------------------------------------------
/.monolinterrc:
--------------------------------------------------------------------------------
1 | {
2 | "include": [
3 | "package.json",
4 | "docs/package.json",
5 | "IapExample/package.json"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | IapExample/
2 | .idea/
3 | gen/
4 | node_modules/
5 | android/react-native-iap.iml
6 | android/android.iml
7 | tsconfig.tsbuildinfo
8 | *.ts
9 | *.tsx
10 | !*.d.ts
11 | tsconfig.json
12 | react-native-iap-*.tgz
13 | .vscode/
14 | docs/
15 | .github/
16 | *.md
17 | .eslintrc.js
18 | .eslintignore
19 | .prettierrc.js
20 | .gitattributes
21 | .yarn/cache
22 | package/
23 | plugin/src
24 | plugin/jest.config.js
25 | plugin/tsconfig.json
26 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | IapExample/vendor
2 | IapExample/ios
3 | IapExample/android
4 | lib
5 | ios
6 | android
7 | docs/.docusaurus
8 | docs/docs/api
9 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | trailingComma: 'all',
3 | arrowParens: 'always',
4 | singleQuote: true,
5 | jsxSingleQuote: false,
6 | bracketSpacing: false,
7 | };
8 |
--------------------------------------------------------------------------------
/.release-it.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | git: {
3 | commitMessage: 'chore: release ${version}',
4 | tagName: 'v${version}',
5 | },
6 | npm: {
7 | publish: true,
8 | },
9 | github: {
10 | release: true,
11 | },
12 | plugins: {
13 | '@release-it/conventional-changelog': {
14 | preset: 'angular',
15 | },
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | opt_in_rules:
2 | - vertical_whitespace_between_cases
3 | - vertical_whitespace_closing_braces
4 | - vertical_whitespace_opening_braces
5 | - vertical_parameter_alignment_on_call
6 | - operator_usage_whitespace
7 | - redundant_type_annotation
8 |
9 | vertical_whitespace_closing_braces: true
10 | vertical_whitespace_opening_braces: true
11 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.validate": [
3 | "javascript",
4 | "javascriptreact",
5 | "typescript",
6 | "typescriptreact"
7 | ],
8 | "editor.codeActionsOnSave": {
9 | "source.fixAll": "explicit"
10 | },
11 | "[javascript]": {
12 | "editor.defaultFormatter": "esbenp.prettier-vscode"
13 | },
14 | "[javascriptreact]": {
15 | "editor.defaultFormatter": "esbenp.prettier-vscode"
16 | },
17 | "[typescript]": {
18 | "editor.defaultFormatter": "esbenp.prettier-vscode"
19 | },
20 | "[typescriptreact]": {
21 | "editor.defaultFormatter": "esbenp.prettier-vscode"
22 | },
23 | "javascript.preferences.importModuleSpecifier": "relative",
24 | "typescript.preferences.importModuleSpecifier": "relative",
25 | "prettier.configPath": ".prettierrc.js",
26 | "cSpell.words": [
27 | "crossplatformkorea",
28 | "hyochan"
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.yarnrc:
--------------------------------------------------------------------------------
1 | # Override Yarn command so we can automatically setup the repo on running `yarn`
2 |
3 | yarn-path "scripts/bootstrap.js"
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Changelog
2 |
3 | Check out the [release notes](https://github.com/hyochan/react-native-iap/releases).
4 |
--------------------------------------------------------------------------------
/IapExample/.bundle/config:
--------------------------------------------------------------------------------
1 | BUNDLE_PATH: "vendor/bundle"
2 | BUNDLE_FORCE_RUBY_PLATFORM: 1
3 |
--------------------------------------------------------------------------------
/IapExample/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: '@react-native',
4 | };
5 |
--------------------------------------------------------------------------------
/IapExample/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 | ios/.xcode.env.local
24 |
25 | # Android/IntelliJ
26 | #
27 | build/
28 | .idea
29 | .gradle
30 | local.properties
31 | *.iml
32 | *.hprof
33 | .cxx/
34 | *.keystore
35 | !debug.keystore
36 |
37 | # node.js
38 | #
39 | node_modules/
40 | npm-debug.log
41 | yarn-error.log
42 |
43 | # fastlane
44 | #
45 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
46 | # screenshots whenever they are needed.
47 | # For more information about the recommended setup visit:
48 | # https://docs.fastlane.tools/best-practices/source-control/
49 |
50 | **/fastlane/report.xml
51 | **/fastlane/Preview.html
52 | **/fastlane/screenshots
53 | **/fastlane/test_output
54 |
55 | # Bundle artifact
56 | *.jsbundle
57 |
58 | # Ruby / CocoaPods
59 | /ios/Pods/
60 | /vendor/bundle/
61 |
62 | # Temporary files created by Metro to check the health of the file watcher
63 | .metro-health-check*
64 |
65 | # testing
66 | /coverage
--------------------------------------------------------------------------------
/IapExample/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | arrowParens: 'avoid',
3 | bracketSameLine: true,
4 | bracketSpacing: false,
5 | singleQuote: true,
6 | trailingComma: 'all',
7 | };
8 |
--------------------------------------------------------------------------------
/IapExample/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/IapExample/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
4 | ruby ">= 2.6.10"
5 |
6 | gem 'cocoapods', '~> 1.13'
7 | gem 'activesupport', '>= 6.1.7.3', '< 7.1.0'
8 |
--------------------------------------------------------------------------------
/IapExample/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.6)
5 | rexml
6 | activesupport (6.1.7.6)
7 | concurrent-ruby (~> 1.0, >= 1.0.2)
8 | i18n (>= 1.6, < 2)
9 | minitest (>= 5.1)
10 | tzinfo (~> 2.0)
11 | zeitwerk (~> 2.3)
12 | addressable (2.8.5)
13 | public_suffix (>= 2.0.2, < 6.0)
14 | algoliasearch (1.27.5)
15 | httpclient (~> 2.8, >= 2.8.3)
16 | json (>= 1.5.1)
17 | atomos (0.1.3)
18 | claide (1.1.0)
19 | cocoapods (1.14.3)
20 | addressable (~> 2.8)
21 | claide (>= 1.0.2, < 2.0)
22 | cocoapods-core (= 1.14.3)
23 | cocoapods-deintegrate (>= 1.0.3, < 2.0)
24 | cocoapods-downloader (>= 2.1, < 3.0)
25 | cocoapods-plugins (>= 1.0.0, < 2.0)
26 | cocoapods-search (>= 1.0.0, < 2.0)
27 | cocoapods-trunk (>= 1.6.0, < 2.0)
28 | cocoapods-try (>= 1.1.0, < 2.0)
29 | colored2 (~> 3.1)
30 | escape (~> 0.0.4)
31 | fourflusher (>= 2.3.0, < 3.0)
32 | gh_inspector (~> 1.0)
33 | molinillo (~> 0.8.0)
34 | nap (~> 1.0)
35 | ruby-macho (>= 2.3.0, < 3.0)
36 | xcodeproj (>= 1.23.0, < 2.0)
37 | cocoapods-core (1.14.3)
38 | activesupport (>= 5.0, < 8)
39 | addressable (~> 2.8)
40 | algoliasearch (~> 1.0)
41 | concurrent-ruby (~> 1.1)
42 | fuzzy_match (~> 2.0.4)
43 | nap (~> 1.0)
44 | netrc (~> 0.11)
45 | public_suffix (~> 4.0)
46 | typhoeus (~> 1.0)
47 | cocoapods-deintegrate (1.0.5)
48 | cocoapods-downloader (2.1)
49 | cocoapods-plugins (1.0.0)
50 | nap
51 | cocoapods-search (1.0.1)
52 | cocoapods-trunk (1.6.0)
53 | nap (>= 0.8, < 2.0)
54 | netrc (~> 0.11)
55 | cocoapods-try (1.2.0)
56 | colored2 (3.1.2)
57 | concurrent-ruby (1.2.2)
58 | escape (0.0.4)
59 | ethon (0.16.0)
60 | ffi (>= 1.15.0)
61 | ffi (1.16.3)
62 | fourflusher (2.3.1)
63 | fuzzy_match (2.0.4)
64 | gh_inspector (1.1.3)
65 | httpclient (2.8.3)
66 | i18n (1.14.1)
67 | concurrent-ruby (~> 1.0)
68 | json (2.7.0)
69 | minitest (5.20.0)
70 | molinillo (0.8.0)
71 | nanaimo (0.3.0)
72 | nap (1.1.0)
73 | netrc (0.11.0)
74 | public_suffix (4.0.7)
75 | rexml (3.2.8)
76 | strscan (>= 3.0.9)
77 | ruby-macho (2.5.1)
78 | strscan (3.1.0)
79 | typhoeus (1.4.1)
80 | ethon (>= 0.9.0)
81 | tzinfo (2.0.6)
82 | concurrent-ruby (~> 1.0)
83 | xcodeproj (1.23.0)
84 | CFPropertyList (>= 2.3.3, < 4.0)
85 | atomos (~> 0.1.3)
86 | claide (>= 1.0.2, < 2.0)
87 | colored2 (~> 3.1)
88 | nanaimo (~> 0.3.0)
89 | rexml (~> 3.2.4)
90 | zeitwerk (2.6.12)
91 |
92 | PLATFORMS
93 | ruby
94 |
95 | DEPENDENCIES
96 | activesupport (>= 6.1.7.3, < 7.1.0)
97 | cocoapods (~> 1.13)
98 |
99 | RUBY VERSION
100 | ruby 2.6.10p210
101 |
102 | BUNDLED WITH
103 | 1.17.2
104 |
--------------------------------------------------------------------------------
/IapExample/README.md:
--------------------------------------------------------------------------------
1 | This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli).
2 |
3 | # Getting Started with IapExample
4 |
5 | > **Note**: Make sure you have completed the [React Native - Environment Setup](https://reactnative.dev/docs/environment-setup) instructions till "Creating a new application" step, before proceeding.
6 |
7 | ## Step 1: Install dependencies
8 |
9 | Install _node_modules_ :
10 |
11 | ```bash
12 | # using npm
13 | npm install
14 |
15 | # OR using Yarn
16 | yarn install
17 | ```
18 |
19 | ### For iOS
20 |
21 | Install pods:
22 |
23 | ```bash
24 | cd ios
25 | pod install
26 | ```
27 |
28 | ## Step 2: Start the Metro Server
29 |
30 | First, you will need to start **Metro**, the JavaScript _bundler_ that ships _with_ React Native.
31 |
32 | To start Metro, run the following command from the _root_ of your React Native project:
33 |
34 | ```bash
35 | # using npm
36 | npm start
37 |
38 | # OR using Yarn
39 | yarn start
40 | ```
41 |
42 | ## Step 3: Start your Application
43 |
44 | Let Metro Bundler run in its _own_ terminal. Open a _new_ terminal from the _root_ of your React Native project. Run the following command to start your _Android_ or _iOS_ app:
45 |
46 | ### For Android
47 |
48 | ```bash
49 | # using npm
50 | npm run android:play
51 |
52 | # OR using Yarn
53 | yarn android:play
54 | ```
55 |
56 | ### For iOS
57 |
58 | ```bash
59 | # using npm
60 | npm run ios
61 |
62 | # OR using Yarn
63 | yarn ios
64 | ```
65 |
66 | If everything is set up _correctly_, you should see your new app running in your _Android Emulator_ or _iOS Simulator_ shortly provided you have set up your emulator/simulator correctly.
67 |
68 | This is one way to run your app — you can also run it directly from within Android Studio and Xcode respectively.
69 |
70 | ## Step 4: Modifying your App
71 |
72 | Now that you have successfully run the app, let's modify it.
73 |
74 | 1. Open `App.tsx` in your text editor of choice and edit some lines.
75 | 2. For **Android**: Press the R key twice or select **"Reload"** from the **Developer Menu** (Ctrl + M (on Window and Linux) or Cmd ⌘ + M (on macOS)) to see your changes!
76 |
77 | For **iOS**: Hit Cmd ⌘ + R in your iOS Simulator to reload the app and see your changes!
78 |
79 | ## Congratulations! :tada:
80 |
81 | You've successfully run and modified your React Native App. :partying_face:
82 |
83 | ### Now what?
84 |
85 | - If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps).
86 | - If you're curious to learn more about React Native, check out the [Introduction to React Native](https://reactnative.dev/docs/getting-started).
87 |
88 | # Troubleshooting
89 |
90 | If you can't get this to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page or try disabling Flipper by setting `NO_FLIPPER=1` in your environment. (e.g. `NO_FLIPPER=1 yarn ios`)
91 |
92 | # Learn More
93 |
94 | To learn more about React Native, take a look at the following resources:
95 |
96 | - [React Native Website](https://reactnative.dev) - learn more about React Native.
97 | - [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
98 | - [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
99 | - [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
100 | - [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.
101 |
--------------------------------------------------------------------------------
/IapExample/__tests__/App.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @format
3 | */
4 |
5 | import 'react-native';
6 | import React from 'react';
7 | import {App} from '../src/App';
8 |
9 | // Note: import explicitly to use the types shiped with jest.
10 | import {it} from '@jest/globals';
11 |
12 | // Note: test renderer must be required after react-native.
13 | import renderer from 'react-test-renderer';
14 |
15 | it('renders correctly', () => {
16 | renderer.create();
17 | });
18 |
--------------------------------------------------------------------------------
/IapExample/android/app/amazon.sdktester.json:
--------------------------------------------------------------------------------
1 | {
2 | "com.amazon.sample.iap.subscription.mymagazine.month": {
3 | "description":"Monthly Subscription to My Magazine",
4 | "title":"My Magazine",
5 | "itemType":"SUBSCRIPTION",
6 | "price":5.0,
7 | "subscriptionParent":"com.amazon.sample.iap.subscription.mymagazine"
8 | },
9 | "com.amazon.sample.iap.subscription.mymagazine.quarter": {
10 | "description":"Quarterly Subscription to My Magazine",
11 | "title":"My Magazine",
12 | "itemType":"SUBSCRIPTION",
13 | "price":12.0,
14 | "subscriptionParent":"com.amazon.sample.iap.subscription.mymagazine"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/IapExample/android/app/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyochan/react-native-iap/a0578ecd9e349ac8959c7b3a2019dc4151dcd0a9/IapExample/android/app/debug.keystore
--------------------------------------------------------------------------------
/IapExample/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
--------------------------------------------------------------------------------
/IapExample/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/IapExample/android/app/src/debug/java/com/iapexample/ReactNativeFlipper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Meta Platforms, Inc. and affiliates.
3 | *
4 | *
This source code is licensed under the MIT license found in the LICENSE file in the root
5 | * directory of this source tree.
6 | */
7 | package com.dooboolab.test;
8 |
9 | import android.content.Context;
10 | import com.facebook.flipper.android.AndroidFlipperClient;
11 | import com.facebook.flipper.android.utils.FlipperUtils;
12 | import com.facebook.flipper.core.FlipperClient;
13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping;
17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
20 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
21 | import com.facebook.react.ReactInstanceEventListener;
22 | import com.facebook.react.ReactInstanceManager;
23 | import com.facebook.react.bridge.ReactContext;
24 | import com.facebook.react.modules.network.NetworkingModule;
25 | import okhttp3.OkHttpClient;
26 |
27 | /**
28 | * Class responsible of loading Flipper inside your React Native application. This is the debug
29 | * flavor of it. Here you can add your own plugins and customize the Flipper setup.
30 | */
31 | public class ReactNativeFlipper {
32 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
33 | if (FlipperUtils.shouldEnableFlipper(context)) {
34 | final FlipperClient client = AndroidFlipperClient.getInstance(context);
35 |
36 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
37 | client.addPlugin(new DatabasesFlipperPlugin(context));
38 | client.addPlugin(new SharedPreferencesFlipperPlugin(context));
39 | client.addPlugin(CrashReporterPlugin.getInstance());
40 |
41 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
42 | NetworkingModule.setCustomClientBuilder(
43 | new NetworkingModule.CustomClientBuilder() {
44 | @Override
45 | public void apply(OkHttpClient.Builder builder) {
46 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
47 | }
48 | });
49 | client.addPlugin(networkFlipperPlugin);
50 | client.start();
51 |
52 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
53 | // Hence we run if after all native modules have been initialized
54 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
55 | if (reactContext == null) {
56 | reactInstanceManager.addReactInstanceEventListener(
57 | new ReactInstanceEventListener() {
58 | @Override
59 | public void onReactContextInitialized(ReactContext reactContext) {
60 | reactInstanceManager.removeReactInstanceEventListener(this);
61 | reactContext.runOnNativeModulesQueueThread(
62 | new Runnable() {
63 | @Override
64 | public void run() {
65 | client.addPlugin(new FrescoFlipperPlugin());
66 | }
67 | });
68 | }
69 | });
70 | } else {
71 | client.addPlugin(new FrescoFlipperPlugin());
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/IapExample/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/IapExample/android/app/src/main/java/com/iapexample/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.dooboolab.test;
2 |
3 | import com.facebook.react.ReactActivity;
4 | import com.facebook.react.ReactActivityDelegate;
5 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
6 | import com.facebook.react.defaults.DefaultReactActivityDelegate;
7 |
8 | public class MainActivity extends ReactActivity {
9 |
10 | /**
11 | * Returns the name of the main component registered from JavaScript. This is used to schedule
12 | * rendering of the component.
13 | */
14 | @Override
15 | protected String getMainComponentName() {
16 | return "IapExample";
17 | }
18 |
19 | /**
20 | * Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
21 | * DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
22 | * (aka React 18) with two boolean flags.
23 | */
24 | @Override
25 | protected ReactActivityDelegate createReactActivityDelegate() {
26 | return new DefaultReactActivityDelegate(
27 | this,
28 | getMainComponentName(),
29 | // If you opted-in for the New Architecture, we enable the Fabric Renderer.
30 | DefaultNewArchitectureEntryPoint.getFabricEnabled());
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/IapExample/android/app/src/main/java/com/iapexample/MainApplication.java:
--------------------------------------------------------------------------------
1 | package com.dooboolab.test;
2 |
3 | import android.app.Application;
4 | import com.facebook.react.PackageList;
5 | import com.facebook.react.ReactApplication;
6 | import com.facebook.react.ReactNativeHost;
7 | import com.facebook.react.ReactPackage;
8 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
9 | import com.facebook.react.defaults.DefaultReactNativeHost;
10 | import com.facebook.soloader.SoLoader;
11 | import java.util.List;
12 |
13 | public class MainApplication extends Application implements ReactApplication {
14 |
15 | private final ReactNativeHost mReactNativeHost =
16 | new DefaultReactNativeHost(this) {
17 | @Override
18 | public boolean getUseDeveloperSupport() {
19 | return BuildConfig.DEBUG;
20 | }
21 |
22 | @Override
23 | protected List getPackages() {
24 | @SuppressWarnings("UnnecessaryLocalVariable")
25 | List packages = new PackageList(this).getPackages();
26 | // Packages that cannot be autolinked yet can be added manually here, for example:
27 | // packages.add(new MyReactNativePackage());
28 | return packages;
29 | }
30 |
31 | @Override
32 | protected String getJSMainModuleName() {
33 | return "index";
34 | }
35 |
36 | @Override
37 | protected boolean isNewArchEnabled() {
38 | return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
39 | }
40 |
41 | @Override
42 | protected Boolean isHermesEnabled() {
43 | return BuildConfig.IS_HERMES_ENABLED;
44 | }
45 | };
46 |
47 | @Override
48 | public ReactNativeHost getReactNativeHost() {
49 | return mReactNativeHost;
50 | }
51 |
52 | @Override
53 | public void onCreate() {
54 | super.onCreate();
55 | SoLoader.init(this, /* native exopackage */ false);
56 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
57 | // If you opted-in for the New Architecture, we load the native entry point for this app.
58 | DefaultNewArchitectureEntryPoint.load();
59 | }
60 | ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/IapExample/android/app/src/main/res/drawable/rn_edit_text_material.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
21 |
22 |
23 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/IapExample/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyochan/react-native-iap/a0578ecd9e349ac8959c7b3a2019dc4151dcd0a9/IapExample/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/IapExample/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyochan/react-native-iap/a0578ecd9e349ac8959c7b3a2019dc4151dcd0a9/IapExample/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/IapExample/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyochan/react-native-iap/a0578ecd9e349ac8959c7b3a2019dc4151dcd0a9/IapExample/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/IapExample/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyochan/react-native-iap/a0578ecd9e349ac8959c7b3a2019dc4151dcd0a9/IapExample/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/IapExample/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyochan/react-native-iap/a0578ecd9e349ac8959c7b3a2019dc4151dcd0a9/IapExample/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/IapExample/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyochan/react-native-iap/a0578ecd9e349ac8959c7b3a2019dc4151dcd0a9/IapExample/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/IapExample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyochan/react-native-iap/a0578ecd9e349ac8959c7b3a2019dc4151dcd0a9/IapExample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/IapExample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyochan/react-native-iap/a0578ecd9e349ac8959c7b3a2019dc4151dcd0a9/IapExample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/IapExample/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyochan/react-native-iap/a0578ecd9e349ac8959c7b3a2019dc4151dcd0a9/IapExample/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/IapExample/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyochan/react-native-iap/a0578ecd9e349ac8959c7b3a2019dc4151dcd0a9/IapExample/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/IapExample/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | IapExample
3 |
4 |
--------------------------------------------------------------------------------
/IapExample/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/IapExample/android/app/src/release/java/com/iapexample/ReactNativeFlipper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Meta Platforms, Inc. and affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the LICENSE file in the root
5 | * directory of this source tree.
6 | */
7 | package com.dooboolab.test;
8 |
9 | import android.content.Context;
10 | import com.facebook.react.ReactInstanceManager;
11 |
12 | /**
13 | * Class responsible of loading Flipper inside your React Native application. This is the release
14 | * flavor of it so it's empty as we don't want to load Flipper.
15 | */
16 | public class ReactNativeFlipper {
17 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
18 | // Do nothing as we don't want to initialize Flipper on Release.
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/IapExample/android/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext {
5 | buildToolsVersion = "33.0.0"
6 | minSdkVersion = 21
7 | compileSdkVersion = 33
8 | targetSdkVersion = 33
9 |
10 | // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
11 | ndkVersion = "23.1.7779620"
12 | }
13 | repositories {
14 | google()
15 | mavenCentral()
16 | }
17 | dependencies {
18 | classpath("com.android.tools.build:gradle")
19 | classpath("com.facebook.react:react-native-gradle-plugin")
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/IapExample/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | # AndroidX package structure to make it clearer which packages are bundled with the
21 | # Android operating system, and which are packaged with your app's APK
22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
23 | android.useAndroidX=true
24 | # Automatically convert third-party libraries to use AndroidX
25 | android.enableJetifier=true
26 |
27 | # Version of flipper SDK to use with React Native
28 | FLIPPER_VERSION=0.182.0
29 |
30 | # Use this property to specify which architecture you want to build.
31 | # You can also override it from the CLI using
32 | # ./gradlew -PreactNativeArchitectures=x86_64
33 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
34 |
35 | # Use this property to enable support to the new architecture.
36 | # This will allow you to use TurboModules and the Fabric render in
37 | # your application. You should enable this flag either if you want
38 | # to write custom TurboModules/Fabric components OR use libraries that
39 | # are providing them.
40 | newArchEnabled=false
41 |
42 | # Use this property to enable or disable the Hermes JS engine.
43 | # If set to false, you will be using JSC instead.
44 | hermesEnabled=true
45 |
--------------------------------------------------------------------------------
/IapExample/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyochan/react-native-iap/a0578ecd9e349ac8959c7b3a2019dc4151dcd0a9/IapExample/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/IapExample/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip
4 | networkTimeout=10000
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/IapExample/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/IapExample/android/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'IapExample'
2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
3 | include ':app'
4 | includeBuild('../node_modules/@react-native/gradle-plugin')
5 |
--------------------------------------------------------------------------------
/IapExample/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "IapExample",
3 | "displayName": "IapExample"
4 | }
5 |
--------------------------------------------------------------------------------
/IapExample/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:@react-native/babel-preset'],
3 | };
4 |
--------------------------------------------------------------------------------
/IapExample/index.tsx:
--------------------------------------------------------------------------------
1 | import {AppRegistry} from 'react-native';
2 |
3 | import {App} from './src/App';
4 | import {name as appName} from './app.json';
5 |
6 | AppRegistry.registerComponent(appName, () => App);
7 |
--------------------------------------------------------------------------------
/IapExample/ios/.xcode.env:
--------------------------------------------------------------------------------
1 | # This `.xcode.env` file is versioned and is used to source the environment
2 | # used when running script phases inside Xcode.
3 | # To customize your local environment, you can create an `.xcode.env.local`
4 | # file that is not versioned.
5 |
6 | # NODE_BINARY variable contains the PATH to the node executable.
7 | #
8 | # Customize the NODE_BINARY variable here.
9 | # For example, to use nvm with brew, add the following line
10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use
11 | export NODE_BINARY=$(command -v node)
12 |
--------------------------------------------------------------------------------
/IapExample/ios/Configuration.storekit:
--------------------------------------------------------------------------------
1 | {
2 | "identifier" : "5BB04EE6",
3 | "nonRenewingSubscriptions" : [
4 | {
5 | "displayPrice" : "9.99",
6 | "familyShareable" : false,
7 | "internalID" : "99AC5E3C",
8 | "localizations" : [
9 | {
10 | "description" : "",
11 | "displayName" : "",
12 | "locale" : "en_US"
13 | }
14 | ],
15 | "productID" : "com.cooni.sub1000",
16 | "referenceName" : "1000 point subscription",
17 | "type" : "NonRenewingSubscription"
18 | }
19 | ],
20 | "products" : [
21 | {
22 | "displayPrice" : "0.99",
23 | "familyShareable" : false,
24 | "internalID" : "FF5725DC",
25 | "localizations" : [
26 | {
27 | "description" : "",
28 | "displayName" : "",
29 | "locale" : "en_US"
30 | }
31 | ],
32 | "productID" : "com.cooni.point1000",
33 | "referenceName" : "1000 points",
34 | "type" : "Consumable"
35 | },
36 | {
37 | "displayPrice" : "2.99",
38 | "familyShareable" : false,
39 | "internalID" : "47DD16EA",
40 | "localizations" : [
41 | {
42 | "description" : "",
43 | "displayName" : "",
44 | "locale" : "en_US"
45 | }
46 | ],
47 | "productID" : "com.cooni.point5000",
48 | "referenceName" : "5000 points",
49 | "type" : "NonConsumable"
50 | }
51 | ],
52 | "settings" : {
53 | "_askToBuyEnabled" : false
54 | },
55 | "subscriptionGroups" : [
56 | {
57 | "id" : "FBD9754C",
58 | "localizations" : [
59 |
60 | ],
61 | "name" : "Point subscription group",
62 | "subscriptions" : [
63 | {
64 | "adHocOffers" : [
65 |
66 | ],
67 | "codeOffers" : [
68 |
69 | ],
70 | "displayPrice" : "29.99",
71 | "familyShareable" : false,
72 | "groupNumber" : 1,
73 | "internalID" : "1FFFCCC1",
74 | "introductoryOffer" : {
75 | "internalID" : "5FF46BC8",
76 | "paymentMode" : "free",
77 | "subscriptionPeriod" : "P1W"
78 | },
79 | "localizations" : [
80 | {
81 | "description" : "",
82 | "displayName" : "",
83 | "locale" : "en_US"
84 | }
85 | ],
86 | "productID" : "com.cooni.sub5000",
87 | "recurringSubscriptionPeriod" : "P1M",
88 | "referenceName" : "5000 Auto-renewable subscription",
89 | "subscriptionGroupID" : "FBD9754C",
90 | "type" : "RecurringSubscription"
91 | }
92 | ]
93 | }
94 | ],
95 | "subscriptionOffersKeyPair" : {
96 | "id" : "9AB2A8DD",
97 | "privateKey" : "MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgQa0Te5HV/F3M2TlphhcZylYlz2tJrL6Esp6CsYNoTOugCgYIKoZIzj0DAQehRANCAATdiy7Y+lM3aFDlLE0reYd5yF2pzZeQJ6h+9jfMDlg5aDOYXEF0978WMYrHYLOcEbwLxqAUpO1FcLu9VR2Vg98t"
98 | },
99 | "version" : {
100 | "major" : 1,
101 | "minor" : 2
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/IapExample/ios/IapExample.xcodeproj/xcshareddata/xcschemes/IapExample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/IapExample/ios/IapExample.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/IapExample/ios/IapExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/IapExample/ios/IapExample/AppDelegate.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | @interface AppDelegate : RCTAppDelegate
5 |
6 | @end
7 |
--------------------------------------------------------------------------------
/IapExample/ios/IapExample/AppDelegate.mm:
--------------------------------------------------------------------------------
1 | #import "AppDelegate.h"
2 |
3 | #import
4 |
5 | @implementation AppDelegate
6 |
7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
8 | {
9 | self.moduleName = @"IapExample";
10 | // You can add your custom initial props in the dictionary below.
11 | // They will be passed down to the ViewController used by React Native.
12 | self.initialProps = @{};
13 |
14 | return [super application:application didFinishLaunchingWithOptions:launchOptions];
15 | }
16 |
17 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
18 | {
19 | #if DEBUG
20 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
21 | #else
22 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
23 | #endif
24 | }
25 |
26 | @end
27 |
--------------------------------------------------------------------------------
/IapExample/ios/IapExample/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ios-marketing",
45 | "scale" : "1x",
46 | "size" : "1024x1024"
47 | }
48 | ],
49 | "info" : {
50 | "author" : "xcode",
51 | "version" : 1
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/IapExample/ios/IapExample/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/IapExample/ios/IapExample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | IapExample
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(CURRENT_PROJECT_VERSION)
25 | LSRequiresIPhoneOS
26 |
27 | NSAppTransportSecurity
28 |
29 | NSExceptionDomains
30 |
31 | localhost
32 |
33 | NSExceptionAllowsInsecureHTTPLoads
34 |
35 |
36 |
37 |
38 | NSLocationWhenInUseUsageDescription
39 |
40 | UILaunchStoryboardName
41 | LaunchScreen
42 | UIRequiredDeviceCapabilities
43 |
44 | armv7
45 |
46 | UISupportedInterfaceOrientations
47 |
48 | UIInterfaceOrientationPortrait
49 | UIInterfaceOrientationLandscapeLeft
50 | UIInterfaceOrientationLandscapeRight
51 |
52 | UIViewControllerBasedStatusBarAppearance
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/IapExample/ios/IapExample/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyAccessedAPITypes
6 |
7 |
8 | NSPrivacyAccessedAPIType
9 | NSPrivacyAccessedAPICategoryFileTimestamp
10 | NSPrivacyAccessedAPITypeReasons
11 |
12 | C617.1
13 |
14 |
15 |
16 | NSPrivacyAccessedAPIType
17 | NSPrivacyAccessedAPICategoryUserDefaults
18 | NSPrivacyAccessedAPITypeReasons
19 |
20 | CA92.1
21 |
22 |
23 |
24 | NSPrivacyAccessedAPIType
25 | NSPrivacyAccessedAPICategorySystemBootTime
26 | NSPrivacyAccessedAPITypeReasons
27 |
28 | 35F9.1
29 |
30 |
31 |
32 | NSPrivacyCollectedDataTypes
33 |
34 | NSPrivacyTracking
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/IapExample/ios/IapExample/main.m:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | #import "AppDelegate.h"
4 |
5 | int main(int argc, char *argv[])
6 | {
7 | @autoreleasepool {
8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/IapExample/ios/IapExampleTests/IapExampleTests.m:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | #import
5 | #import
6 |
7 | #define TIMEOUT_SECONDS 600
8 | #define TEXT_TO_LOOK_FOR @"Welcome to React"
9 |
10 | @interface IapExampleTests : XCTestCase
11 |
12 | @end
13 |
14 | @implementation IapExampleTests
15 |
16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test
17 | {
18 | if (test(view)) {
19 | return YES;
20 | }
21 | for (UIView *subview in [view subviews]) {
22 | if ([self findSubviewInView:subview matching:test]) {
23 | return YES;
24 | }
25 | }
26 | return NO;
27 | }
28 |
29 | - (void)testRendersWelcomeScreen
30 | {
31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController];
32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS];
33 | BOOL foundElement = NO;
34 |
35 | __block NSString *redboxError = nil;
36 | #ifdef DEBUG
37 | RCTSetLogFunction(
38 | ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
39 | if (level >= RCTLogLevelError) {
40 | redboxError = message;
41 | }
42 | });
43 | #endif
44 |
45 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) {
46 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
47 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
48 |
49 | foundElement = [self findSubviewInView:vc.view
50 | matching:^BOOL(UIView *view) {
51 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) {
52 | return YES;
53 | }
54 | return NO;
55 | }];
56 | }
57 |
58 | #ifdef DEBUG
59 | RCTSetLogFunction(RCTDefaultLogFunction);
60 | #endif
61 |
62 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError);
63 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS);
64 | }
65 |
66 | @end
67 |
--------------------------------------------------------------------------------
/IapExample/ios/IapExampleTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/IapExample/ios/Podfile:
--------------------------------------------------------------------------------
1 | require 'xcodeproj'
2 |
3 | # Resolve react_native_pods.rb with node to allow for hoisting
4 | require Pod::Executable.execute_command('node', ['-p',
5 | 'require.resolve(
6 | "react-native/scripts/react_native_pods.rb",
7 | {paths: [process.argv[1]]},
8 | )', __dir__]).strip
9 |
10 | project_path = './IapExample.xcodeproj'
11 | project = Xcodeproj::Project.open(project_path)
12 |
13 | # Fetches minimum deployment target version from the project and sets it as the default
14 | config_list = project.root_object.build_configuration_list
15 | debug_config = config_list.build_configurations.find { |config| config.name == 'Debug' }
16 | min_ios_version = debug_config.build_settings['IPHONEOS_DEPLOYMENT_TARGET']
17 | default_min_ios_version = '12.4'
18 | if min_ios_version.nil? || min_ios_version.empty?
19 | puts "IPHONEOS_DEPLOYMENT_TARGET not set at the project level for Debug configuration. Using default value of #{default_min_ios_version}"
20 | min_ios_version = default_min_ios_version
21 | else
22 | puts "Minimum iOS version set to: #{min_ios_version}"
23 | end
24 | platform :ios, min_ios_version || default_min_ios_version
25 |
26 | prepare_react_native_project!
27 |
28 | linkage = ENV['USE_FRAMEWORKS']
29 | if linkage != nil
30 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
31 | use_frameworks! :linkage => linkage.to_sym
32 | end
33 |
34 | target 'IapExample' do
35 | config = use_native_modules!
36 |
37 | # Flags change depending on the env values.
38 | flags = get_default_flags()
39 |
40 | use_react_native!(
41 | :path => config[:reactNativePath],
42 | # Hermes is now enabled by default. Disable by setting this flag to false.
43 | :hermes_enabled => flags[:hermes_enabled],
44 | :fabric_enabled => flags[:fabric_enabled],
45 | # An absolute path to your application root.
46 | :app_path => "#{Pod::Config.instance.installation_root}/.."
47 | )
48 |
49 | target 'IapExampleTests' do
50 | inherit! :complete
51 | # Pods for testing
52 | end
53 |
54 | post_install do |installer|
55 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
56 | react_native_post_install(
57 | installer,
58 | config[:reactNativePath],
59 | :mac_catalyst_enabled => false
60 | )
61 | __apply_Xcode_12_5_M1_post_install_workaround(installer)
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/IapExample/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'react-native',
3 | };
4 |
--------------------------------------------------------------------------------
/IapExample/metro.config.js:
--------------------------------------------------------------------------------
1 | const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
2 |
3 | /**
4 | * Metro configuration
5 | * https://facebook.github.io/metro/docs/configuration
6 | *
7 | * @type {import('metro-config').MetroConfig}
8 | */
9 | const config = {};
10 |
11 | module.exports = mergeConfig(getDefaultConfig(__dirname), config);
12 |
--------------------------------------------------------------------------------
/IapExample/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "iap-example",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "android:play": "react-native run-android --variant=GooglePlayDebug",
7 | "android:amazon": "react-native run-android --variant=AmazonDebug",
8 | "ios": "react-native run-ios",
9 | "lint": "eslint .",
10 | "start": "react-native start",
11 | "test": "jest",
12 | "pods": "pod-install",
13 | "postinstall": "rm -rf node_modules/react-native-iap/node_modules"
14 | },
15 | "dependencies": {
16 | "@react-native-community/cli-platform-android": "^12.2.0",
17 | "@react-native-masked-view/masked-view": "^0.3.0",
18 | "@react-navigation/native": "^6.1.9",
19 | "@react-navigation/stack": "^6.3.20",
20 | "react-native-iap": "file:..",
21 | "lodash": "^4.17.21",
22 | "react": "18.2.0",
23 | "react-native": "0.72.17",
24 | "react-native-gesture-handler": "^2.14.0",
25 | "react-native-safe-area-context": "^4.7.4",
26 | "react-native-screens": "^3.27.0"
27 | },
28 | "devDependencies": {
29 | "@babel/core": "^7.20.0",
30 | "@babel/preset-env": "^7.20.0",
31 | "@babel/runtime": "^7.26.10",
32 | "@react-native/eslint-config": "^0.72.2",
33 | "@react-native/metro-config": "^0.72.11",
34 | "@tsconfig/react-native": "^3.0.0",
35 | "@types/lodash": "^4.14.202",
36 | "@types/react": "^18.0.24",
37 | "@types/react-test-renderer": "^18.0.0",
38 | "babel-jest": "^29.2.1",
39 | "eslint": "^8.19.0",
40 | "jest": "^29.2.1",
41 | "metro-react-native-babel-preset": "0.76.8",
42 | "pod-install": "^0.1.39",
43 | "prettier": "^2.4.1",
44 | "react-test-renderer": "18.2.0",
45 | "typescript": "4.8.4"
46 | },
47 | "engines": {
48 | "node": ">=16"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/IapExample/react-native.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | dependencies: {
5 | 'react-native-iap': {
6 | root: path.join(__dirname, '..'),
7 | },
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/IapExample/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {setup} from 'react-native-iap';
3 | import {NavigationContainer} from '@react-navigation/native';
4 |
5 | import {StackNavigator} from './navigators';
6 | setup({storekitMode: 'STOREKIT2_MODE'});
7 |
8 | export const App = () => (
9 |
10 |
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/IapExample/src/components/Box.tsx:
--------------------------------------------------------------------------------
1 | import React, {ReactNode} from 'react';
2 | import {View} from 'react-native';
3 |
4 | import {theme} from '../utils';
5 |
6 | interface BoxProps {
7 | children: ReactNode;
8 | }
9 |
10 | export const Box = ({children}: BoxProps) => (
11 | {children}
12 | );
13 |
--------------------------------------------------------------------------------
/IapExample/src/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Button as RNButton, StyleSheet, View} from 'react-native';
3 |
4 | import {borderRadius, colors} from '../utils';
5 |
6 | interface ButtonProps {
7 | title: string;
8 |
9 | onPress(): void;
10 | }
11 |
12 | export const Button = ({title, onPress}: ButtonProps) => (
13 |
14 |
15 |
16 | );
17 |
18 | const styles = StyleSheet.create({
19 | button: {
20 | backgroundColor: colors.gray100,
21 | borderRadius: borderRadius - 2,
22 | },
23 | });
24 |
--------------------------------------------------------------------------------
/IapExample/src/components/Heading.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {StyleSheet, Text, View} from 'react-native';
3 |
4 | import {borderRadius, colors, theme} from '../utils';
5 |
6 | interface HeadingProps {
7 | copy: string;
8 | label?: string;
9 | }
10 |
11 | export const Heading = ({copy, label}: HeadingProps) => (
12 |
13 | {copy}
14 | {label && {label}}
15 |
16 | );
17 |
18 | const styles = StyleSheet.create({
19 | heading: {
20 | flexDirection: 'row',
21 | justifyContent: 'space-between',
22 | paddingVertical: 8,
23 | paddingHorizontal: 12,
24 | backgroundColor: colors.gray100,
25 | borderRadius: borderRadius - 2,
26 | },
27 |
28 | label: {
29 | ...theme.L1,
30 | color: colors.gray600,
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/IapExample/src/components/Row.tsx:
--------------------------------------------------------------------------------
1 | import React, {ReactNode} from 'react';
2 | import {StyleSheet, Text, View, ViewStyle} from 'react-native';
3 | import camelCase from 'lodash/camelCase';
4 |
5 | import {theme} from '../utils';
6 |
7 | interface RowField {
8 | label: string;
9 | value: string;
10 | }
11 |
12 | interface RowProps {
13 | children?: ReactNode;
14 | fields: RowField[];
15 | flexDirection?: ViewStyle['flexDirection'];
16 | isLast?: boolean;
17 | }
18 |
19 | export const Row = ({
20 | children,
21 | fields,
22 | flexDirection = 'row',
23 | isLast,
24 | }: RowProps) => (
25 |
26 |
27 | {fields.map((field, index) => (
28 |
31 | {field.label}
32 | {field.value}
33 |
34 | ))}
35 |
36 |
37 | {children}
38 |
39 | );
40 |
41 | const styles = StyleSheet.create({
42 | row: {
43 | marginBottom: 10,
44 | },
45 |
46 | rowLast: {
47 | marginBottom: 0,
48 | },
49 | });
50 |
--------------------------------------------------------------------------------
/IapExample/src/components/State.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Text} from 'react-native';
3 |
4 | import {theme} from '../utils';
5 |
6 | import {Box} from './Box';
7 |
8 | interface StateProps {
9 | connected: boolean;
10 | storekit2?: boolean;
11 | }
12 |
13 | export const State = ({connected, storekit2}: StateProps) => {
14 | const stateText =
15 | (connected ? 'connected' : 'not connected') +
16 | (storekit2 ? ' | Storekit 2' : '');
17 | return (
18 |
19 | State
20 | {stateText}
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/IapExample/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Box';
2 | export * from './Button';
3 | export * from './Heading';
4 | export * from './Row';
5 | export * from './State';
6 |
--------------------------------------------------------------------------------
/IapExample/src/navigators/StackNavigator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {withIAPContext} from 'react-native-iap';
3 | import {createStackNavigator} from '@react-navigation/stack';
4 |
5 | import {
6 | ClassSetup,
7 | Examples,
8 | Products,
9 | PurchaseHistory,
10 | Subscriptions,
11 | } from '../screens';
12 | import {AvailablePurchases} from '../screens/AvailablePurchases';
13 |
14 | export const examples = [
15 | {
16 | name: 'Products',
17 | label: 'With success and error listeners.',
18 | component: withIAPContext(Products),
19 | section: 'Context',
20 | color: '#47d371',
21 | emoji: '💵',
22 | },
23 | {
24 | name: 'Subscriptions',
25 | component: withIAPContext(Subscriptions),
26 | section: 'Context',
27 | color: '#cebf38',
28 | emoji: '💳',
29 | },
30 | {
31 | name: 'PurchaseHistory',
32 | component: withIAPContext(PurchaseHistory),
33 | section: 'Context',
34 | color: '#c241b3',
35 | emoji: '📄',
36 | },
37 | {
38 | name: 'AvailablePurchases',
39 | component: withIAPContext(AvailablePurchases),
40 | section: 'Context',
41 | color: '#475ed3',
42 | emoji: '🧾',
43 | },
44 | {
45 | name: 'ClassSetup',
46 | component: ClassSetup,
47 | section: 'Others',
48 | color: '#b947d3',
49 | emoji: '',
50 | },
51 | ] as const;
52 |
53 | export type Screens = {
54 | Examples: undefined;
55 | Products: undefined;
56 | Subscriptions: undefined;
57 | PurchaseHistory: undefined;
58 | AvailablePurchases: undefined;
59 | Listeners: undefined;
60 | ClassSetup: undefined;
61 | };
62 |
63 | const Stack = createStackNavigator();
64 |
65 | export const StackNavigator = () => (
66 |
67 |
68 |
69 | {examples.map(({name, component}) => (
70 |
79 | ))}
80 |
81 | );
82 |
--------------------------------------------------------------------------------
/IapExample/src/navigators/index.ts:
--------------------------------------------------------------------------------
1 | export * from './StackNavigator';
2 |
--------------------------------------------------------------------------------
/IapExample/src/screens/AvailablePurchases.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {ScrollView, StyleSheet, View} from 'react-native';
3 | import {useIAP} from 'react-native-iap';
4 |
5 | import {Box, Button, Heading, Row, State} from '../components';
6 | import {contentContainerStyle, errorLog} from '../utils';
7 |
8 | export const AvailablePurchases = () => {
9 | const {connected, availablePurchases, getAvailablePurchases} = useIAP();
10 |
11 | const handleGetAvailablePurchases = async () => {
12 | try {
13 | await getAvailablePurchases();
14 | } catch (error) {
15 | errorLog({message: 'handleGetAvailablePurchases', error});
16 | }
17 | };
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {availablePurchases.map((availablePurchase, index) => (
28 |
38 | ))}
39 |
40 |
41 |
45 |
46 |
47 | );
48 | };
49 |
50 | const styles = StyleSheet.create({
51 | container: {
52 | marginBottom: 20,
53 | },
54 | });
55 |
--------------------------------------------------------------------------------
/IapExample/src/screens/Examples.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Pressable,
4 | SectionList,
5 | SectionListData,
6 | SectionListRenderItem,
7 | StyleSheet,
8 | Text,
9 | View,
10 | } from 'react-native';
11 | import {useNavigation} from '@react-navigation/native';
12 | import type {StackNavigationProp} from '@react-navigation/stack';
13 | import lowerCase from 'lodash/lowerCase';
14 | import upperFirst from 'lodash/upperFirst';
15 |
16 | import {examples, Screens} from '../navigators';
17 | import {colors, theme} from '../utils';
18 |
19 | interface SectionData {
20 | name: keyof Screens;
21 | label?: string;
22 | component: JSX.Element;
23 | color: string;
24 | emoji: string;
25 | }
26 |
27 | interface ReducedData {
28 | section: string;
29 | data: SectionData[];
30 | }
31 |
32 | export const Examples = () => {
33 | const {navigate} = useNavigation>();
34 | const data = examples.reduce((acc, cur) => {
35 | const sectionFound = acc.find(({section}) => section === cur.section);
36 |
37 | if (sectionFound) {
38 | // @ts-ignore
39 | sectionFound.data.push(cur);
40 | } else {
41 | // @ts-ignore
42 | acc.push({section: cur.section, data: [cur]});
43 | }
44 |
45 | return acc;
46 | }, []);
47 |
48 | const renderItem: SectionListRenderItem = ({
49 | item,
50 | }) => (
51 | navigate(item.name)}>
52 |
53 | {item.emoji && {item.emoji}}
54 |
55 |
56 | {upperFirst(lowerCase(item.name))}
57 |
58 |
59 | {item.label && {item.label}}
60 |
61 |
62 | );
63 |
64 | const renderSection = ({
65 | section,
66 | }: {
67 | section: SectionListData;
68 | }) => (
69 |
70 | {section.section}
71 |
72 | );
73 |
74 | return (
75 |
81 | );
82 | };
83 |
84 | const styles = StyleSheet.create({
85 | container: {
86 | backgroundColor: '#f0f0f0',
87 | },
88 |
89 | content: {
90 | paddingBottom: 32,
91 | },
92 |
93 | row: {
94 | backgroundColor: colors.white,
95 | paddingVertical: 18,
96 | paddingHorizontal: 20,
97 | borderBottomWidth: StyleSheet.hairlineWidth,
98 | borderColor: colors.gray200,
99 | },
100 |
101 | rowTitle: {
102 | ...theme.H3,
103 | marginTop: 4,
104 | },
105 |
106 | rowLabel: {
107 | ...theme.P2,
108 | marginTop: 4,
109 | },
110 |
111 | sectionRow: {
112 | paddingVertical: 8,
113 | paddingHorizontal: 10,
114 | backgroundColor: colors.gray200,
115 | },
116 |
117 | sectionText: {
118 | ...theme.L1,
119 | color: '#454545',
120 | },
121 | });
122 |
--------------------------------------------------------------------------------
/IapExample/src/screens/PurchaseHistory.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {ScrollView, StyleSheet, View} from 'react-native';
3 | import {useIAP} from 'react-native-iap';
4 |
5 | import {Box, Button, Heading, Row, State} from '../components';
6 | import {contentContainerStyle, errorLog} from '../utils';
7 |
8 | export const PurchaseHistory = () => {
9 | const {connected, purchaseHistory, getPurchaseHistory} = useIAP();
10 |
11 | const handleGetPurchaseHistory = async () => {
12 | try {
13 | await getPurchaseHistory();
14 | } catch (error) {
15 | errorLog({message: 'handleGetPurchaseHistory', error});
16 | }
17 | };
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {purchaseHistory.map((purchase, index) => (
28 |
38 | ))}
39 |
40 |
41 |
45 |
46 |
47 | );
48 | };
49 |
50 | const styles = StyleSheet.create({
51 | container: {
52 | marginBottom: 20,
53 | },
54 | });
55 |
--------------------------------------------------------------------------------
/IapExample/src/screens/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ClassSetup';
2 | export * from './Examples';
3 | export * from './Products';
4 | export * from './PurchaseHistory';
5 | export * from './Subscriptions';
6 |
--------------------------------------------------------------------------------
/IapExample/src/utils/constants.ts:
--------------------------------------------------------------------------------
1 | import {Platform} from 'react-native';
2 |
3 | import {isAmazon} from 'react-native-iap/src/internal';
4 |
5 | const productSkus = Platform.select({
6 | ios: ['com.cooni.point1000', 'com.cooni.point5000'],
7 |
8 | android: [
9 | 'android.test.purchased',
10 | 'android.test.canceled',
11 | 'android.test.refunded',
12 | 'android.test.item_unavailable',
13 | ],
14 |
15 | default: [],
16 | }) as string[];
17 |
18 | const subscriptionSkus = Platform.select({
19 | ios: ['com.cooni.sub1000', 'com.cooni.sub5000'],
20 | android: isAmazon
21 | ? [
22 | 'com.amazon.sample.iap.subscription.mymagazine.month',
23 | 'com.amazon.sample.iap.subscription.mymagazine.quarter',
24 | ]
25 | : ['test.sub1'],
26 | default: [],
27 | }) as string[];
28 | const amazonBaseSku = 'com.amazon.sample.iap.subscription.mymagazine';
29 | export const constants = {
30 | productSkus,
31 | subscriptionSkus,
32 | amazonBaseSku,
33 | };
34 |
--------------------------------------------------------------------------------
/IapExample/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './constants';
2 | export * from './logs';
3 | export * from './platform';
4 | export * from './theme';
5 |
--------------------------------------------------------------------------------
/IapExample/src/utils/logs.ts:
--------------------------------------------------------------------------------
1 | export const errorLog = ({
2 | message,
3 | error,
4 | }: {
5 | message: string;
6 | error: unknown;
7 | }) => {
8 | console.error('An error happened', message, error);
9 | };
10 |
--------------------------------------------------------------------------------
/IapExample/src/utils/platform.ts:
--------------------------------------------------------------------------------
1 | import {
2 | isAmazon,
3 | isAndroid,
4 | isIos,
5 | isPlay,
6 | } from 'react-native-iap/src/internal';
7 | export {isAmazon, isIos, isAndroid, isPlay};
8 |
--------------------------------------------------------------------------------
/IapExample/src/utils/theme.ts:
--------------------------------------------------------------------------------
1 | import {StyleSheet} from 'react-native';
2 |
3 | export const contentContainerStyle = {paddingBottom: 40};
4 |
5 | export const borderRadius = 8;
6 |
7 | export const colors = {
8 | white: '#fff',
9 | dark: '#000',
10 | gray100: '#f0f0f0',
11 | gray200: '#e0e0e0',
12 | gray300: '#aaa',
13 | gray500: '#454545',
14 | gray600: '#212121',
15 | blue: '#3994d0',
16 | red: '#df4343',
17 | green: '#48c982',
18 | };
19 |
20 | export const theme = StyleSheet.create({
21 | /**
22 | * Headings
23 | */
24 |
25 | H2: {
26 | fontSize: 18,
27 | lineHeight: 26,
28 | color: colors.gray600,
29 | },
30 |
31 | H3: {
32 | fontSize: 16,
33 | lineHeight: 24,
34 | color: colors.gray500,
35 | },
36 |
37 | /**
38 | * Paragraphs
39 | */
40 |
41 | P1: {
42 | fontSize: 16,
43 | lineHeight: 20,
44 | color: colors.dark,
45 | },
46 |
47 | P2: {
48 | fontSize: 14,
49 | lineHeight: 18,
50 | color: colors.gray300,
51 | },
52 |
53 | /**
54 | * Labels
55 | */
56 |
57 | L1: {
58 | textTransform: 'uppercase',
59 | letterSpacing: 0.3,
60 | fontWeight: '600',
61 | fontSize: 12,
62 | color: colors.gray300,
63 | },
64 |
65 | /**
66 | * Layouts
67 | */
68 |
69 | row: {
70 | marginTop: 20,
71 | paddingBottom: 20,
72 | flexDirection: 'row',
73 | alignItems: 'center',
74 | justifyContent: 'space-between',
75 | borderBottomWidth: 1,
76 | borderBottomColor: colors.gray100,
77 | },
78 |
79 | rowLast: {
80 | paddingBottom: 0,
81 | borderBottomWidth: 0,
82 | borderBottomColor: 'transparent',
83 | },
84 |
85 | /**
86 | * Atoms
87 | */
88 |
89 | box: {
90 | margin: 20,
91 | marginBottom: 5,
92 | padding: 20,
93 | backgroundColor: colors.white,
94 | borderRadius,
95 | shadowColor: 'rgba(0, 0, 0, 0.45)',
96 | shadowOffset: {height: 16, width: 0},
97 | shadowOpacity: 0.1,
98 | shadowRadius: 12,
99 | },
100 |
101 | button: {
102 | padding: 10,
103 | backgroundColor: colors.blue,
104 | borderRadius: borderRadius - 2,
105 | },
106 | });
107 |
--------------------------------------------------------------------------------
/IapExample/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/react-native/tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 hyochan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/RNIap.podspec:
--------------------------------------------------------------------------------
1 | require "json"
2 |
3 | package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4 | folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
5 |
6 | Pod::Spec.new do |s|
7 | s.name = "RNIap"
8 | s.version = package["version"]
9 | s.summary = package["description"]
10 | s.homepage = package["homepage"]
11 | s.license = package["license"]
12 | s.authors = package["author"]
13 |
14 | s.platforms = { :ios => "10.0" , :tvos => "10.0", :visionos => "1.0" }
15 | s.source = { :git => "https://github.com/hyochan/react-native-iap.git", :tag => "#{s.version}" }
16 |
17 | s.source_files = "ios/*.{h,m,mm,swift}"
18 |
19 | s.requires_arc = true
20 |
21 | s.dependency "React-Core"
22 |
23 | # Don't install the dependencies when we run `pod install` in the old architecture.
24 | if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
25 | s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
26 | s.pod_target_xcconfig = {
27 | "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
28 | "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
29 | }
30 |
31 | s.dependency "RCT-Folly"
32 | s.dependency "RCTRequired"
33 | s.dependency "RCTTypeSafety"
34 | s.dependency "ReactCommon/turbomodule/core"
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | RNIap_kotlinVersion=1.8.0
2 | RNIap_minSdkVersion=21
3 | RNIap_targetSdkVersion=33
4 | RNIap_compileSdkVersion=33
5 | RNIap_buildToolsVersion=33.0.0
6 | RNIap_ndkversion=23.1.7779620
7 | RNIap_playServicesVersion=18.1.0
8 | RNIap_amazonSdkVersion=3.0.5
9 | RNIap_playBillingSdkVersion=7.0.0
10 | RNIap_isAmazonDrmEnabled=true
11 |
12 | android.useAndroidX=true
13 | android.enableJetifier=true
14 |
--------------------------------------------------------------------------------
/android/src/amazon/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/android/src/amazon/java/com/dooboolab/rniap/EventSender.kt:
--------------------------------------------------------------------------------
1 | package com.dooboolab.rniap
2 |
3 | import com.facebook.react.bridge.WritableMap
4 |
5 | interface EventSender {
6 | fun sendEvent(
7 | eventName: String,
8 | params: WritableMap?,
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/android/src/amazon/java/com/dooboolab/rniap/PurchasingServiceProxy.kt:
--------------------------------------------------------------------------------
1 | package com.dooboolab.rniap
2 |
3 | import android.content.Context
4 | import com.amazon.device.iap.PurchasingListener
5 | import com.amazon.device.iap.model.FulfillmentResult
6 | import com.amazon.device.iap.model.RequestId
7 |
8 | interface PurchasingServiceProxy {
9 | fun registerListener(
10 | var0: Context?,
11 | var1: PurchasingListener?,
12 | )
13 |
14 | fun getUserData(): RequestId
15 |
16 | fun purchase(var0: String?): RequestId
17 |
18 | fun getProductData(var0: Set?): RequestId
19 |
20 | fun getPurchaseUpdates(var0: Boolean): RequestId
21 |
22 | fun notifyFulfillment(
23 | var0: String?,
24 | var1: FulfillmentResult?,
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/android/src/amazon/java/com/dooboolab/rniap/PurchasingServiceProxyAmazonImpl.kt:
--------------------------------------------------------------------------------
1 | package com.dooboolab.rniap
2 |
3 | import android.content.Context
4 | import com.amazon.device.iap.PurchasingListener
5 | import com.amazon.device.iap.PurchasingService
6 | import com.amazon.device.iap.model.FulfillmentResult
7 | import com.amazon.device.iap.model.RequestId
8 |
9 | class PurchasingServiceProxyAmazonImpl : PurchasingServiceProxy {
10 | override fun registerListener(
11 | var0: Context?,
12 | var1: PurchasingListener?,
13 | ) = PurchasingService.registerListener(var0, var1)
14 |
15 | override fun getUserData(): RequestId = PurchasingService.getUserData()
16 |
17 | override fun purchase(var0: String?): RequestId = PurchasingService.purchase(var0)
18 |
19 | override fun getProductData(var0: Set?): RequestId = PurchasingService.getProductData(var0)
20 |
21 | override fun getPurchaseUpdates(var0: Boolean): RequestId = PurchasingService.getPurchaseUpdates(var0)
22 |
23 | override fun notifyFulfillment(
24 | var0: String?,
25 | var1: FulfillmentResult?,
26 | ) = PurchasingService.notifyFulfillment(var0, var1)
27 | }
28 |
--------------------------------------------------------------------------------
/android/src/amazon/java/com/dooboolab/rniap/RNIapActivityListener.kt:
--------------------------------------------------------------------------------
1 | package com.dooboolab.rniap
2 |
3 | import android.app.Activity
4 | import android.util.Log
5 | import com.amazon.device.iap.PurchasingService
6 |
7 | /**
8 | * In order of the IAP process to show correctly, AmazonPurchasingService must be registered on Activity.onCreate
9 | * registering it in on Application.onCreate will not throw an error but it will now show the Native purchasing screen
10 | */
11 | class RNIapActivityListener {
12 | companion object {
13 | @JvmStatic
14 | var hasListener = false
15 |
16 | @JvmStatic
17 | var amazonListener: RNIapAmazonListener? = null
18 |
19 | @JvmStatic
20 | fun registerActivity(activity: Activity) {
21 | amazonListener = RNIapAmazonListener(null, null)
22 | try {
23 | PurchasingService.registerListener(activity, amazonListener)
24 | hasListener = true
25 | // Prefetch user and purchases as per Amazon SDK documentation:
26 | PurchasingService.getUserData()
27 | PurchasingService.getPurchaseUpdates(false)
28 | } catch (e: Exception) {
29 | Log.e(RNIapAmazonModule.TAG, "Error initializing Amazon appstore sdk", e)
30 | }
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/android/src/amazon/java/com/dooboolab/rniap/RNIapPackage.kt:
--------------------------------------------------------------------------------
1 | package com.dooboolab.rniap
2 |
3 | import com.facebook.react.ReactPackage
4 | import com.facebook.react.bridge.NativeModule
5 | import com.facebook.react.bridge.ReactApplicationContext
6 | import com.facebook.react.uimanager.ViewManager
7 | import java.util.ArrayList
8 |
9 | class RNIapPackage : ReactPackage {
10 | override fun createViewManagers(reactContext: ReactApplicationContext): List> = emptyList()
11 |
12 | override fun createNativeModules(reactContext: ReactApplicationContext): List {
13 | val modules: MutableList = ArrayList()
14 | modules.add(RNIapAmazonModule(reactContext))
15 | return modules
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/android/src/main/java/com/dooboolab/rniap/PromiseUtils.kt:
--------------------------------------------------------------------------------
1 | package com.dooboolab.rniap
2 |
3 | import com.facebook.react.bridge.Promise
4 | import java.lang.Exception
5 | import java.util.HashMap
6 |
7 | object PromiseUtils {
8 | private val promises = HashMap>()
9 |
10 | fun addPromiseForKey(
11 | key: String,
12 | promise: Promise,
13 | ) {
14 | promises.getOrPut(key) { mutableListOf() }.add(promise)
15 | }
16 |
17 | fun resolvePromisesForKey(
18 | key: String,
19 | value: Any?,
20 | ) {
21 | promises[key]?.forEach { promise ->
22 | promise.safeResolve(value)
23 | }
24 | promises.remove(key)
25 | }
26 |
27 | fun rejectAllPendingPromises() {
28 | promises.flatMap { it.value }.forEach { promise ->
29 | promise.safeReject(E_CONNECTION_CLOSED, "Connection has been closed", null)
30 | }
31 | promises.clear()
32 | }
33 |
34 | fun rejectPromisesForKey(
35 | key: String,
36 | code: String,
37 | message: String?,
38 | err: Exception?,
39 | ) {
40 | promises[key]?.forEach { promise ->
41 | promise.safeReject(code, message, err)
42 | }
43 | promises.remove(key)
44 | }
45 |
46 | private const val TAG = "PromiseUtils"
47 | const val E_UNKNOWN = "E_UNKNOWN"
48 | const val E_NOT_PREPARED = "E_NOT_PREPARED"
49 | const val E_ALREADY_PREPARED = "E_ALREADY_PREPARED"
50 | const val E_PENDING = "E_PENDING"
51 | const val E_NOT_ENDED = "E_NOT_ENDED"
52 | const val E_USER_CANCELLED = "E_USER_CANCELLED"
53 | const val E_ITEM_UNAVAILABLE = "E_ITEM_UNAVAILABLE"
54 | const val E_NETWORK_ERROR = "E_NETWORK_ERROR"
55 | const val E_SERVICE_ERROR = "E_SERVICE_ERROR"
56 | const val E_ALREADY_OWNED = "E_ALREADY_OWNED"
57 | const val E_REMOTE_ERROR = "E_REMOTE_ERROR"
58 | const val E_USER_ERROR = "E_USER_ERROR"
59 | const val E_DEVELOPER_ERROR = "E_DEVELOPER_ERROR"
60 | const val E_BILLING_RESPONSE_JSON_PARSE_ERROR = "E_BILLING_RESPONSE_JSON_PARSE_ERROR"
61 | const val E_CONNECTION_CLOSED = "E_CONNECTION_CLOSED"
62 | }
63 |
--------------------------------------------------------------------------------
/android/src/main/java/com/dooboolab/rniap/PromiseUtlis.kt:
--------------------------------------------------------------------------------
1 | package com.dooboolab.rniap
2 |
3 | import android.util.Log
4 | import com.facebook.react.bridge.Promise
5 | import java.lang.RuntimeException
6 |
7 | /**
8 | * Extension functions used to simplify promise handling since we don't
9 | * want to crash in the case of it being resolved/rejected more than once
10 | */
11 |
12 | const val TAG = "IapPromises"
13 |
14 | fun Promise.safeResolve(value: Any?) {
15 | try {
16 | this.resolve(value)
17 | } catch (e: RuntimeException) {
18 | Log.d(TAG, "Already consumed ${e.message}")
19 | }
20 | }
21 |
22 | fun Promise.safeReject(message: String) = this.safeReject(message, null, null)
23 |
24 | fun Promise.safeReject(
25 | code: String,
26 | message: String?,
27 | ) = this.safeReject(code, message, null)
28 |
29 | fun Promise.safeReject(
30 | code: String,
31 | throwable: Throwable?,
32 | ) = this.safeReject(code, null, throwable)
33 |
34 | fun Promise.safeReject(
35 | code: String,
36 | message: String?,
37 | throwable: Throwable?,
38 | ) {
39 | try {
40 | this.reject(code, message, throwable)
41 | } catch (e: RuntimeException) {
42 | Log.d(TAG, "Already consumed ${e.message}")
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/android/src/play/java/com/dooboolab/rniap/RNIapActivityListener.kt:
--------------------------------------------------------------------------------
1 | package com.dooboolab.rniap
2 |
3 | import android.app.Activity
4 |
5 | /**
6 | * Currently only needed for Amazon IAP
7 | */
8 | class RNIapActivityListener {
9 | companion object {
10 | @JvmStatic
11 | fun registerActivity(activity: Activity) {
12 | // No op
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/android/src/play/java/com/dooboolab/rniap/RNIapPackage.kt:
--------------------------------------------------------------------------------
1 | package com.dooboolab.rniap
2 |
3 | import com.facebook.react.ReactPackage
4 | import com.facebook.react.bridge.NativeModule
5 | import com.facebook.react.bridge.ReactApplicationContext
6 | import com.facebook.react.uimanager.ViewManager
7 | import java.util.ArrayList
8 |
9 | class RNIapPackage : ReactPackage {
10 | override fun createViewManagers(reactContext: ReactApplicationContext): List> = emptyList()
11 |
12 | override fun createNativeModules(reactContext: ReactApplicationContext): List {
13 | val modules: MutableList = ArrayList()
14 | modules.add(RNIapModule(reactContext))
15 | return modules
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app.plugin.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./plugin/build/withIAP');
2 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:@react-native/babel-preset'],
3 | };
4 |
--------------------------------------------------------------------------------
/bob.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | source: 'src',
3 | output: 'lib',
4 | targets: [
5 | 'commonjs',
6 | 'module',
7 | [
8 | 'typescript',
9 | {
10 | project: 'tsconfig.build.json',
11 | },
12 | ],
13 | ],
14 | };
15 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | /node_modules
3 |
4 | # Production
5 | /build
6 |
7 | # Generated files
8 | .docusaurus
9 | .cache-loader
10 |
11 | # Misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
22 | # yarn2
23 | .pnp.*
24 | .yarn/*
25 | !.yarn/patches
26 | !.yarn/plugins
27 | !.yarn/releases
28 | !.yarn/sdks
29 | !.yarn/versions
30 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Website
2 |
3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
4 |
5 | ## Installation
6 |
7 | ```console
8 | yarn install
9 | ```
10 |
11 | ## Local Development
12 |
13 | ```console
14 | yarn start
15 | ```
16 |
17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
18 |
19 | ## Build
20 |
21 | ```console
22 | yarn build
23 | ```
24 |
25 | This command generates static content into the `build` directory and can be served using any static contents hosting service.
26 |
27 | ## Deployment
28 |
29 | ```console
30 | GIT_USER= USE_SSH=true yarn deploy
31 | ```
32 |
33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
34 |
--------------------------------------------------------------------------------
/docs/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
3 | };
4 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "API Reference",
3 | "position": 3
4 | }
5 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/hooks.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 6
3 | ---
4 |
5 | # Hooks
6 |
7 | ## Installation
8 |
9 | You first have to wrap your app with the `withIAPContext` HOC.
10 |
11 | ```tsx
12 | import React from 'react';
13 | import {withIAPContext} from 'react-native-iap';
14 |
15 | const App = () => ;
16 |
17 | export default withIAPContext(App);
18 | ```
19 |
20 | ## Usage
21 |
22 | The `useIAP()` hook is an easy way to access `react-native-iap` methods simplified for you. It already does some work through the context to help you get your products, purchases, subscriptions, callback and error handlers faster.
23 |
24 | Below are all the methods available through the hook. All the rest of the methods e.g. `requestPurchase` are available through the usual import `import {requestPurchase} from 'react-native-iap';`
25 |
26 | ```tsx
27 | import React from 'react';
28 | import {View, Text} from 'react-native';
29 | import {requestPurchase, useIAP} from 'react-native-iap';
30 |
31 | const App = () => {
32 | const {
33 | connected,
34 | products,
35 | promotedProductsIOS,
36 | subscriptions,
37 | purchaseHistory,
38 | availablePurchases,
39 | currentPurchase,
40 | currentPurchaseError,
41 | initConnectionError,
42 | finishTransaction,
43 | getProducts,
44 | getSubscriptions,
45 | getAvailablePurchases,
46 | getPurchaseHistory,
47 | } = useIAP();
48 |
49 | const handlePurchase = async (sku: string) => {
50 | await requestPurchase({sku});
51 | };
52 |
53 | useEffect(() => {
54 | // ... listen to currentPurchaseError, to check if any error happened
55 | }, [currentPurchaseError]);
56 |
57 | useEffect(() => {
58 | // ... listen to currentPurchase, to check if the purchase went through
59 | }, [currentPurchase]);
60 |
61 | return (
62 | <>
63 |
67 |
68 | {products.map((product) => (
69 |
70 | {product.productId}
71 |
72 |
77 | ))}
78 | >
79 | );
80 | };
81 | ```
82 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Methods"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/amazon/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Amazon",
3 | "position": 13
4 | }
5 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/amazon/validate-receipt-amazon.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Validate Receipt Amazon
3 | sidebar_label: Validate Receipt Amazon
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # `validateReceiptAmazon`
11 |
12 | Validate receipt.
13 |
14 | :::note
15 | This method is here for debugging purposes only. Including your
16 | developer secret in the binary you ship to users is potentially dangerous.
17 | Use server-side validation instead for your production builds.
18 | :::
19 |
20 | ## Signature
21 |
22 | ```ts
23 | validateReceiptAmazon(
24 | /** From the Amazon developer console */
25 | developerSecret: string,
26 |
27 | /** Who purchased the item. */
28 | userId: string,
29 |
30 | /** Long obfuscated string returned when purchasing the item */
31 | receiptId: string,
32 |
33 | /** Defaults to true, use sandbox environment or production. */
34 | useSandbox: boolean = true,
35 | ): Promise;
36 | ```
37 |
38 | ## Usage
39 |
40 | ```tsx
41 | import React from 'react';
42 | import {Button} from 'react-native';
43 | import {validateReceiptAmazon} from 'react-native-iap';
44 |
45 | const App = () => {
46 | const handlePurchase = async () => {
47 | const response = await validateReceiptAmazon(
48 | 'your-developer-secret',
49 | 'user-id',
50 | 'receipt-id',
51 | );
52 | };
53 |
54 | return ;
55 | };
56 | ```
57 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/android/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Android",
3 | "position": 12
4 | }
5 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/android/acknowledge-purchase-android.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Acknowledge Purchase Android
3 | sidebar_label: acknowledgePurchaseAndroid
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # `acknowledgePurchaseAndroid`
11 |
12 | Acknowledge a product.
13 |
14 | ## Signature
15 |
16 | ```ts
17 | acknowledgePurchaseAndroid(
18 | /** The product's token */
19 | token: string,
20 |
21 | /** Android developerPayload */
22 | developerPayload?: string,
23 | ): Promise;
24 | ```
25 |
26 | ## Usage
27 |
28 | ```tsx
29 | import React from 'react';
30 | import {Button} from 'react-native';
31 | import {acknowledgePurchaseAndroid} from 'react-native-iap';
32 |
33 | const App = () => {
34 | const handlePurchase = async () => {
35 | await acknowledgePurchaseAndroid({
36 | token: 'token',
37 | developerPayload: 'developer-payload',
38 | });
39 | };
40 |
41 | return ;
42 | }
43 | ```
44 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/android/deep-link-to-subscriptions-android.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Deep link to subscriptions
3 | sidebar_label: deepLinkToSubscriptionsAndroid
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # `deepLinkToSubscriptionsAndroid`
11 |
12 | Deep link to subscriptions screen.
13 |
14 | ## Signature
15 |
16 | ```ts
17 | deepLinkToSubscriptionsAndroid(
18 | /** The product's SKU */
19 | sku: Sku,
20 | ): Promise;
21 | ```
22 |
23 | ## Usage
24 |
25 | ```tsx
26 | import React from 'react';
27 | import {Button} from 'react-native';
28 | import {deepLinkToSubscriptionsAndroid} from 'react-native-iap';
29 |
30 | const App = () => {
31 | const handleSubscriptions = async () => {
32 | await deepLinkToSubscriptionsAndroid({sku: 'sku-id'});
33 | };
34 |
35 | return ;
36 | };
37 | ```
38 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/android/flush-failed-purchases-cached-as-pending-android.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Flush failed purchases cached as pending (Android)
3 | sidebar_label: flushFailedPurchasesCachedAsPendingAndroid
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # `flushFailedPurchasesCachedAsPendingAndroid`
11 |
12 | Consume all 'ghost' purchases.
13 |
14 | That is, pending payment that already failed but is still marked as pending in Play Store cache.
15 |
16 | ## Signature
17 |
18 | ```ts
19 | flushFailedPurchasesCachedAsPendingAndroid(): Promise;
20 | ```
21 |
22 | ## Usage
23 |
24 | ```tsx
25 | import React from 'react';
26 | import {Button} from 'react-native';
27 | import {flushFailedPurchasesCachedAsPendingAndroid} from 'react-native-iap';
28 |
29 | const App = () => {
30 | const handleFlush = async () => {
31 | await flushFailedPurchasesCachedAsPendingAndroid();
32 | };
33 |
34 | return (
35 |
36 | );
37 | }
38 | ```
39 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/android/validate-receipt-android.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Validate Receipt Android
3 | sidebar_label: validateReceiptAndroid
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # `validateReceiptAndroid`
11 |
12 | Validate receipt.
13 |
14 | :::note
15 | This method is here for debugging purposes only. Including your
16 | access token in the binary you ship to users is potentially dangerous.
17 | Use server side validation instead for your production builds.
18 | :::
19 |
20 | ## Signature
21 |
22 | ```ts
23 | validateReceiptAndroid(
24 | /** package name of your app. */
25 | packageName: string,
26 |
27 | /** product id for your in app product. */
28 | productId: string,
29 |
30 | /** token for your purchase. */
31 | productToken: string,
32 |
33 | /** accessToken from googleApis. */
34 | accessToken: string,
35 |
36 | /** whether this is a subscription or in-app product. `true` for subscription. */
37 | isSub?: boolean,
38 | ): Promise;
39 | ```
40 |
41 | ## Usage
42 |
43 | ```tsx
44 | import React from 'react';
45 | import {Button} from 'react-native';
46 | import {validateReceiptAndroid} from 'react-native-iap';
47 |
48 | const App = () => {
49 | const handlePurchase = async () => {
50 | const response = await validateReceiptAndroid({
51 | packageName: purchase.packageNameAndroid,
52 | productId: purchase.productId,
53 | productToken: purchase.purchaseToken,
54 | accessToken: 'your-access-token',
55 | isSub: true
56 | });
57 | };
58 |
59 | return ;
60 | }
61 | ```
62 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/ios/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "iOS",
3 | "position": 11
4 | }
5 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/ios/buy-promoted-product.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Buy Promoted Product
3 | sidebar_label: buyPromotedProductIOS
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # `buyPromotedProductIOS`
11 |
12 | Buy the currently selected promoted product.
13 |
14 | Initiates the payment process for a promoted product.
15 |
16 | :::note
17 | Should only be called in response to the `iap-promoted-product` event.
18 | :::
19 |
20 | ## Signature
21 |
22 | ```ts
23 | buyPromotedProductIOS(): Promise
24 | ```
25 |
26 | ## Usage
27 |
28 | ```tsx
29 | import React from 'react';
30 | import {Button} from 'react-native';
31 | import {buyPromotedProductIOS} from 'react-native-iap';
32 |
33 | const App = () => {
34 | const handleBuy = async () => await buyPromotedProductIOS();
35 |
36 | return (
37 |
38 | );
39 | }
40 | ```
41 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/ios/clear-products-ios.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Clear Products IOS
3 | sidebar_label: clearProductsIOS
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # `clearProductsIOS`
11 |
12 | Clear valid products.
13 |
14 | Remove all products which are validated by Apple server.
15 |
16 | ## Signature
17 |
18 | ```ts
19 | clearProductsIOS(): Promise
20 | ```
21 |
22 | ## Usage
23 |
24 | ```tsx
25 | import React, {useEffect} from 'react';
26 | import {View} from 'react-native';
27 | import {clearProductsIOS} from 'react-native-iap';
28 |
29 | const App = () => {
30 | useEffect(() => {
31 | void clearProductsIOS();
32 | }, []);
33 |
34 | return ;
35 | }
36 | ```
37 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/ios/clear-transaction-ios.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Clear Transaction IOS
3 | sidebar_label: clearTransactionIOS
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # `clearTransactionIOS`
11 |
12 | Clear the remaining transactions.
13 |
14 | See https://github.com/hyochan/react-native-iap/issues/257
15 | See https://github.com/hyochan/react-native-iap/issues/801
16 |
17 | ## Signature
18 |
19 | ```ts
20 | clearTransactionIOS(): Promise
21 | ```
22 |
23 | ## Usage
24 |
25 | ```tsx
26 | import React, {useEffect} from 'react';
27 | import {View} from 'react-native';
28 | import {clearTransactionIOS} from 'react-native-iap';
29 |
30 | const App = () => {
31 | useEffect(() => {
32 | void clearTransactionIOS();
33 | }, [])
34 |
35 | return ;
36 | }
37 | ```
38 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/ios/get-pending-purchases-ios.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Get Pending Purchases IOS
3 | sidebar_label: getPendingPurchasesIOS
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # `getPendingPurchasesIOS`
11 |
12 | Gets all the transactions which are pending to be finished.
13 |
14 | ## Signature
15 |
16 | ```ts
17 | getPendingPurchasesIOS(): Promise;
18 | ```
19 |
20 | ## Usage
21 |
22 | ```tsx
23 | import React from 'react';
24 | import {Button} from 'react-native';
25 | import {getPendingPurchasesIOS} from 'react-native-iap';
26 |
27 | const App = () => {
28 | const handlePendingPurchases = async () => await getPendingPurchasesIOS();
29 |
30 | return (
31 |
32 | )
33 | }
34 | ```
35 |
36 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/ios/get-promoted-product-ios.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Get Promoted Product IOS
3 | sidebar_label: getPromotedProductIOS
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # `getPromotedProductIOS`
11 |
12 | Should get products promoted on the App Store.
13 |
14 | Indicates the the App Store purchase should continue from the app instead of the App Store.
15 |
16 | ## Signature
17 |
18 | ```ts
19 | getPromotedProductIOS(): Promise;
20 | ```
21 |
22 | ## Usage
23 |
24 | ```tsx
25 | import React, {useCallback} from 'react';
26 | import {View} from 'react-native';
27 | import {getPromotedProductIOS} from 'react-native-iap';
28 |
29 | const App = () => {
30 | const promotedProduct = useCallback(async () => await getPromotedProductIOS());
31 |
32 | return ;
33 | }
34 | ```
35 |
36 | TODO: works with listener to get the products
37 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/ios/present-code-redemption-sheet-ios.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Present Code Redemption Sheet IOS
3 | sidebar_label: presentCodeRedemptionSheetIOS
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # `presentCodeRedemptionSheetIOS`
11 |
12 | Displays a sheet that enables users to redeem subscription offer codes that you generated in App Store Connect.
13 |
14 | Availability: `iOS 14.0+`
15 |
16 | ## Signature
17 |
18 | ```ts
19 | presentCodeRedemptionSheetIOS(): Promise;
20 | ```
21 |
22 | ## Usage
23 |
24 | ```tsx
25 | import React from 'react';
26 | import {Button} from 'react-native';
27 | import {presentCodeRedemptionSheetIOS} from 'react-native-iap';
28 |
29 | const App = () => {
30 | const handleRedemption = async () => {
31 | await presentCodeRedemptionSheetIOS();
32 | }
33 |
34 | return (
35 |
36 | )
37 | }
38 | ```
39 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/ios/request-purchase-with-offer.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Request Purchase With Offer IOS
3 | sidebar_label: requestPurchaseWithOfferIOS
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # `requestPurchaseWithOfferIOS`
11 |
12 | Buy products or subscriptions with offers.
13 |
14 | Runs the payment process with some info you must fetch from your server.
15 |
16 | ## Signature
17 |
18 | ```ts
19 | requestPurchaseWithOfferIOS(
20 | /** The product identifier */
21 | sku: Sku,
22 |
23 | /** An user identifier on you system */
24 | forUser: string,
25 |
26 | /** The offer information */
27 | withOffer: PaymentDiscount,
28 | ): Promise
29 | ```
30 |
31 | ## Usage
32 |
33 | ```tsx
34 | import React from 'react';
35 | import {Button} from 'react-native';
36 | import {requestPurchaseWithOfferIOS} from 'react-native-iap';
37 |
38 | const App = () => {
39 | const handlePurchase = async () => {
40 | await requestPurchaseWithOfferIOS({sku: 'productId', forUser: 'user-id', withOffer: {
41 | identifier: 'string',
42 | keyIdentifier: 'string',
43 | nonce: 'string',
44 | signature: 'string',
45 | timestamp: Date.now(),
46 | }});
47 | }
48 |
49 | return (
50 |
51 | );
52 | }
53 | ```
54 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/ios/request-purchase-with-quantity-ios.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Request Purchase With Quantity IOS
3 | sidebar_label: requestPurchaseWithQuantityIOS
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # `requestPurchaseWithQuantityIOS`
11 |
12 | Request a purchase with a quantity for a product.
13 |
14 | The response will be received through the `PurchaseUpdatedListener`.
15 |
16 | ## Signature
17 |
18 | ```ts
19 | requestPurchaseWithQuantityIOS(
20 | /** The product's sku/ID */
21 | sku: Sku,
22 |
23 | /** The quantity to request to buy */
24 | quantity: number,
25 | ): Promise
26 | ```
27 |
28 | ## Usage
29 |
30 | ```tsx
31 | import React from 'react';
32 | import {Button} from 'react-native';
33 | import {requestPurchaseWithQuantityIOS} from 'react-native-iap';
34 |
35 | const App = () => {
36 | const handlePurchase = async () => {
37 | await requestPurchaseWithQuantityIOS('productId', 2);
38 | }
39 |
40 | return (
41 |
42 | );
43 | }
44 | ```
45 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/ios/validate-receipt-ios.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Validate Receipt IOS
3 | sidebar_label: validateReceiptIOS
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # `validateReceiptIOS`
11 |
12 | Validate receipt.
13 |
14 | ## Signature
15 |
16 | ```ts
17 | validateReceiptIOS(
18 | /** The receipt body to send to apple server. */
19 | receiptBody: Record,
20 |
21 | /** Whether this is in test environment which is sandbox. */
22 | isTest?: boolean,
23 | ): Promise
24 | ```
25 |
26 | ## Usage
27 |
28 | ```tsx
29 | import React from 'react';
30 | import {Button} from 'react-native';
31 | import {validateReceiptIOS} from 'react-native-iap';
32 |
33 | const App = () => {
34 | const handleValidate = async () => {
35 | await validateReceiptIOS({
36 | 'receipt-data': '...',
37 | });
38 | }
39 |
40 | return (
41 |
42 | );
43 | }
44 | ```
45 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/listeners/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Listeners",
3 | "position": 10
4 | }
5 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/listeners/promoted-product-listener.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Promoted Product Listener
3 | sidebar_label: promotedProductListener
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # `promotedProductListener`
11 |
12 | Add IAP promoted subscription event.
13 |
14 | ## Signature
15 |
16 | ```ts
17 | promotedProductListener((productId?: string) => {});
18 | ```
19 |
20 | ## Usage
21 |
22 | ```tsx
23 | import React, {useEffect} from 'react';
24 | import {View} from 'react-native';
25 | import {promotedProductListener} from 'react-native-iap';
26 |
27 | const App = () => {
28 | useEffect(() => {
29 | const subscription = promotedProductListener((productId) => {
30 | console.log(productId);
31 | });
32 |
33 | return () => {
34 | subscription.remove();
35 | };
36 | }, []);
37 |
38 | return ;
39 | };
40 | ```
41 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/listeners/purchase-error-listener.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Purchase Error Listener
3 | sidebar_label: purchaseErrorListener
4 | sidebar_position: 10
5 | ---
6 |
7 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
8 |
9 |
10 |
11 | # `purchaseErrorListener`
12 |
13 | Register a callback that gets called when there has been an error with a purchase. Returns a React Native `EmitterSubscription` on which you can call `.remove()` to stop receiving updates.
14 |
15 | ## Signature
16 |
17 | ```ts
18 | purchaseErrorListener((error: PurchaseError) => {});
19 | ```
20 |
21 | ## Usage
22 |
23 | ```tsx
24 | import React, {useEffect} from 'react';
25 | import {View} from 'react-native';
26 | import {purchaseErrorListener} from 'react-native-iap';
27 |
28 | const App = () => {
29 | useEffect(() => {
30 | const subscription = purchaseErrorListener((error: PurchaseError) => {
31 | console.log(error);
32 | });
33 |
34 | return () => {
35 | subscription.remove();
36 | };
37 | }, []);
38 |
39 | return ;
40 | };
41 | ```
42 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/methods/listeners/purchase-updated-listener.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Purchase Updated Listener
3 | sidebar_label: purchaseUpdatedListener
4 | sidebar_position: 11
5 | ---
6 |
7 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
8 |
9 |
10 |
11 | # `purchaseUpdatedListener`
12 |
13 | Register a callback that gets called when the store has any updates to purchases that have not yet been finished, consumed or acknowledged. Returns a React Native `EmitterSubscription` on which you can call `.remove()` to stop receiving updates. Register you listener as soon as possible and react to updates at all times.
14 |
15 | ## Signature
16 |
17 | ```ts
18 | purchaseUpdatedListener((purchase: Purchase) => {});
19 | ```
20 |
21 | ## Usage
22 |
23 | ```tsx
24 | import React, {useEffect} from 'react';
25 | import {View} from 'react-native';
26 | import {purchaseUpdatedListener} from 'react-native-iap';
27 |
28 | const App = () => {
29 | useEffect(() => {
30 | const subscription = purchaseUpdatedListener((purchase: Purchase) => {
31 | console.log(purchase);
32 | });
33 |
34 | return () => {
35 | subscription.remove();
36 | };
37 | }, []);
38 |
39 | return ;
40 | };
41 | ```
42 |
--------------------------------------------------------------------------------
/docs/docs/guides/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Guides",
3 | "position": 2
4 | }
5 |
--------------------------------------------------------------------------------
/docs/docs/guides/amazon-iap.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Amazon IAP
3 | sidebar_label: Amazon IAP
4 | sidebar_position: 7
5 | ---
6 |
7 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
8 |
9 |
10 |
11 | # Amazon IAP
12 |
13 | The guide assumes that `react-native-iap` is implemented in your app and works with the Play Store without issues. Here are the additional steps to configure Amazon IAP.
14 |
15 | ### Add In-App Items for your app
16 |
17 | 1. Create "In-App Items" using Amazon Developer portal for your app. Amazon put up detailed instructions at https://developer.amazon.com/docs/in-app-purchasing/iap-create-and-submit-iap-items.html
18 |
19 | 2. Add this a call to `RNIapActivityListener.registerActivity(this);` inside your `MainActivity`'s `onCreate` method. This is a necessary step only when using Amazon, but adding it will not affect negatively your Google Play Android builds. E.g.:
20 |
21 | ```java
22 |
23 | import com.dooboolab.rniap.RNIapActivityListener;
24 | ...
25 | public class MainActivity extends ReactActivity {
26 | ...
27 | @Override
28 | protected void onCreate(Bundle savedInstanceState) {
29 | super.onCreate(savedInstanceState);
30 | //Needed for Amazon IAP
31 | RNIapActivityListener.registerActivity(this);
32 | }
33 | ```
34 |
35 | 3. Add new `SKU` strings to your `Iap.getProducts` or `Iap.getSubscriptions` calls.
36 |
37 | ### App configuration
38 |
39 | 1. The current version of Amazon IAP SDK does not play well with R8 optimization. (https://developer.amazon.com/docs/in-app-purchasing/iap-obfuscate-the-code.html).
40 |
41 | Add the code below in `android/app/proguard-rules.pro`:
42 |
43 | ```diff
44 | + -dontwarn com.amazon.**
45 | + -keep class com.amazon.** {*;}
46 | + -keepattributes *Annotation*
47 | ```
48 |
49 | ## Amazon's DRM protection
50 |
51 | This package includes Amazon's DRM (Digital Rights Management) protection enabled by default. Amazon's DRM allows license verification for your app, ensuring compliance and protection of your digital content. For more details, refer to the following resources:
52 | 1. [DRM Overview](https://developer.amazon.com/docs/in-app-purchasing/drm-overview.html)
53 | 2. [DRM for Android](https://developer.amazon.com/docs/in-app-purchasing/drm-android.html)
54 |
55 | ### Disabling Amazon's DRM
56 |
57 | In certain cases, you may prefer not to use Amazon's DRM solution and instead implement your own custom DRM. To disable Amazon's DRM, simply add the following property to your project's `gradle.properties` file:
58 |
59 | ```java
60 | isAmazonDrmEnabled=false
61 | ```
62 |
63 | This setting overrides the default behavior, disabling Amazon's DRM for your app.
64 |
65 | ## Testing in development
66 |
67 | To run the example app, with the amazon provider, run:
68 |
69 | ```bash npm2yarn
70 | npm run android:amazon
71 | ```
72 |
73 | Amazon offers the `App Tester` tool to make In-App purchases testing easier. More information can be found [here](https://developer.amazon.com/docs/in-app-purchasing/iap-app-tester-user-guide.html).
74 |
75 | ## Server Validation
76 |
77 | Amazon IAP API supports validation of In-App purchases on a remote server side. More information can be found [here](https://developer.amazon.com/docs/in-app-purchasing/iap-rvs-for-android-apps.html).
78 |
79 | ## Subscriptions
80 |
81 | When fetching subscriptions from Amazon, make sure to use children `SKUs` (so SKUs for specific period ex. monthly or annually), do not use parent subscription `SKUs`!
82 |
83 | ## Caveats
84 |
85 | Amazon does not return decimal price & currency. Only localized price as a string (ex. 11.22$), see this [page](https://forums.developer.amazon.com/answers/234257/view.html).
86 |
87 | The package will try its best to parse the string into decimal price. If the package cannot parse the price, it will be 0. Currency is detected based on users Amazon marketplace.
88 |
--------------------------------------------------------------------------------
/docs/docs/guides/lifecycle.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Lifecycle
3 | sidebar_label: Lifecycle
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # Lifecycle
11 |
12 | ## Initializing
13 |
14 | In order to initialize the native modules, call `initConnection()` early in the lifecycle of your application. This should be done at a top-level component as the library caches the native connection. Initializing just before you needed is discouraged as it incurs on a performance hit. Calling this method multiple times without ending the previous connection will result in an error. Not calling this method will cause other method calls to be rejected as connection needs to be established ahead of time.
15 |
16 | ```ts
17 | import {initConnection} from 'react-native-iap';
18 |
19 | componentDidMount() {
20 | initConnection();
21 | // ...
22 | }
23 | ```
24 |
25 | ## Ending Connection
26 |
27 | In order to release the resources, call `endConnection()` when you no longer need any interaction with the library.
28 |
29 | ```ts
30 | import {endConnection} from 'react-native-iap';
31 |
32 | componentWillUnmount() {
33 | // ...
34 | endConnection();
35 | }
36 | ```
37 |
38 | ## Dos and Don'ts
39 |
40 | You should not call `initConnection` and `endConnection` every time you need to interact with the library. This is considered an anti-pattern as it consumes more time, resources and could lead to undesired side effects such as many callbacks.
41 |
42 | ### :white_check_mark: DO:
43 |
44 | ```ts
45 | import {initConnection,getProducts,endConnection} from 'react-native-iap';
46 |
47 | componentDidMount() {
48 | await initConnection();
49 | await getProducts(productIds)
50 | // ...
51 | }
52 |
53 | buyProductButtonClick() {
54 | // start purchase code...
55 | }
56 |
57 | subscribeButtonClick() {
58 | // start purchase code...
59 | }
60 |
61 | componentWillUnmount() {
62 | // ...
63 | endConnection();
64 | }
65 | ```
66 |
67 | ### :x: DON'T :
68 |
69 | ```ts
70 | import {initConnection,getProducts,endConnection,initConnection,getProducts,endConnection} from 'react-native-iap';
71 |
72 | componentDidMount() {
73 | // ...
74 | }
75 |
76 | const buyProductButtonClick = async() => {
77 | await initConnection();
78 | await getProducts(productIds)
79 | // Purchase IAP Code...
80 | await endConnection();
81 | }
82 |
83 | const subscribeButtonClick = async() => {
84 | await initConnection();
85 | await getProducts(productIds)
86 | // Purchase Subscription Code...
87 | await endConnection();
88 | }
89 |
90 | componentWillUnmount() {
91 | // ...
92 | }
93 | ```
94 |
--------------------------------------------------------------------------------
/docs/docs/guides/receipts.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Receipts
3 | sidebar_label: Receipts
4 | sidebar_position: 5
5 | ---
6 |
7 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
8 |
9 |
10 |
11 | # Receipts
12 |
13 | ### With Google Play
14 |
15 | For Android, you need separate json file from the service account to get the
16 | `access_token` from `google-apis`, therefore it is impossible to implement serverless.
17 |
18 | You should have your own backend and get `access_token`.
19 | With `access_token` you can simply call `validateReceiptAndroid()` we implemented.
20 | Further reading is [here](https://stackoverflow.com/questions/35127086) or refer to [example repo](https://github.com/Bang9/android-get-access-token-example).
21 |
22 | ### With App Store
23 |
24 | #### Local Validation
25 |
26 | Local on-device cryptographic validation is not currently supported. More details are here: https://developer.apple.com/documentation/appstorereceipts/validating_receipts_on_the_device
27 |
28 | #### Validating with the App Store
29 |
30 | > WARNING: This method is not recommended for production usage, and Apple explicitly warn against it in their docs: https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/validating_receipts_with_the_app_store
31 |
32 | This can be used as a convenience method for developing and testing receipt validation through the development lifecycle.
33 |
34 | Currently, validating receipts with the App Store is possible locally using `validateReceiptIos()`.
35 |
36 | - The first parameter, you should pass `transactionReceipt` which returns after `buyProduct()`.
37 | - The second parameter, you should pass whether this is `test` environment.
38 | If `true`, it will request to `sandbox` and `false` it will request to `production`.
39 |
40 | ```ts
41 | const receiptBody = {
42 | 'receipt-data': purchase.transactionReceipt,
43 | password: '******', // app shared secret, can be found in App Store Connect
44 | };
45 | const result = await RNIap.validateReceiptIos(receiptBody, false);
46 | console.log(result);
47 | ```
48 |
49 | For further information, please refer to [guide](https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html).
50 |
51 | Sometimes you will need to get the receipt at times other than after purchase.
52 | For example, when a user needs to ask for permission to buy a product (`Ask to buy`
53 | flow) or unstable internet connections.
54 |
55 | For these cases we have a convenience method `getReceiptIOS()` which gets
56 | the latest receipt for the app at any given time. The response is base64 encoded.
57 |
58 | ### iOS Purchasing process right way.
59 |
60 | Issue regarding `valid products`
61 |
62 | - In iOS, generally you are fetching valid products at App launching process.
63 |
64 | If you fetch again, or fetch valid subscription, the products are added to
65 | the array object in iOS side (Objective-C `NSMutableArray`).
66 |
67 | This makes unexpected behavior when you fetch with a part of product lists.
68 |
69 | For example, if you have products of `[A, B, C]`, and you call fetch function
70 | with only `[A]`, this module returns `[A, B, C]`).
71 |
72 | This is weird, but it works.
73 |
74 | - But, weird result is weird, so we made a new method which remove all valid products.
75 |
76 | If you need to clear all products, subscriptions in that array, just call
77 | `clearProductsIOS()`, and do the fetching job again, and you will receive what
78 | you expected.
79 |
80 | ### Example backend (Node.js)
81 |
82 | [Here](https://github.com/mifi/in-app-subscription-example) you can find an example backend for idempotent validating of receipts on both iOS/Android and storing and serving subscription state to the client.
83 |
--------------------------------------------------------------------------------
/docs/docs/guides/troubleshooting.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Troubleshooting
3 | sidebar_label: Troubleshooting
4 | sidebar_position: 8
5 | ---
6 |
7 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
8 |
9 |
10 |
11 | # Troubleshooting
12 |
13 | ## Recommendations
14 |
15 | - Before continuing any coding, ensure that you have successfully completed all forms related to agreements, bank accounts, or tax information (any bureaucratic processes associated with the app store you are using).
16 |
17 | ## Common issues
18 |
19 | Most of the issues encountered by users are caused by:
20 |
21 | - A device simulator. Use a real device for testing!
22 | - An incorrect usage of the library. Read the [documentation](https://react-native-iap.hyo.dev).
23 |
24 | ## `getProducts` returns an empty array
25 |
26 | - Please double check if you've called `initConnection`.
27 | - Please wait for max. 24 hours to fetch your IAP products if you've just uploaded them. [Related to issue](https://github.com/hyochan/react-native-iap/issues/1065).
28 | - For `iOS`, from iOS version `>=13`, we seem to use `StoreKit` to fix this issue as [mentioned in stackoverflow](https://stackoverflow.com/questions/58020258/requesting-an-in-app-purchase-in-ios-13-fails/58065711#58065711).
29 | - For `android`, please double check [issue comment here](https://github.com/hyochan/react-native-iap/issues/124#issuecomment-386593185) and see if you've missed something.
30 |
31 | ## `getAvailablePurchases()` returns an empty array
32 |
33 | - `getAvailablePurchases()` is used only when you purchase a non-consumable product. This can be restored only.
34 | - If you want to find out if a user subscribes the product, you should check the receipt which you should store in your own database.
35 | - Apple suggests you handle this in your own backend to do things like what you are trying to achieve.
36 |
37 | ## Invalid `productId` in iOS.
38 |
39 | Please try below and make sure you've done all the steps:
40 |
41 | 1. Completed an effective "Agreements, Tax, and Banking."
42 | 2. Setup sandbox testing account in "Users and Roles."
43 | 3. Signed into iOS device with sandbox account in "Settings / iTunes & App Stores".
44 |
45 | 4. Set up three In-App Purchases with the following status:
46 |
47 | - Ready to Submit
48 | - Missing Metadata
49 | - Waiting for Review
50 |
51 | 5. Enable "In-App Purchase" in Xcode "Capabilities" and in Apple Developer -> "App ID" setting.
52 |
53 | 6. Clean up builds:
54 | - Delete the app on device
55 | - Restart device
56 | - Quit “store” related processes in Activity Monitor
57 | - Development Provisioning Profile -> Clean -> Build.
58 |
59 | - Related issues [#256](https://github.com/hyochan/react-native-iap/issues/256), [#263](https://github.com/hyochan/react-native-iap/issues/263).
60 |
--------------------------------------------------------------------------------
/docs/docs/migrate_to_10.0.0.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Migrating to 10.0.0
3 | sidebar_label: Migrating to 10.0.0
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # Migrating to 10.0.0
11 |
12 | Starting with 10.0.0, the parameters to some of the methods are now objects instead of positional parameters.
13 |
14 | ## Before
15 |
16 | ```ts
17 | getProducts(['my_sku']);
18 | ```
19 |
20 | ## After
21 |
22 | ```ts
23 | getProducts({skus: ['my_sku']});
24 | ```
25 |
26 | Methods are now exported outside of the main module:
27 |
28 | ## Before
29 |
30 | ```ts
31 | import IAP from 'react-native-iap'
32 | ...
33 |
34 | IAP.requestPurchase(...)
35 | ```
36 |
37 | ## After
38 |
39 | ```ts
40 | import {requestPurchase} from 'react-native-iap';
41 | ...
42 | requestPurchase(...)
43 | ```
44 |
45 | If you want to import keeping the namespace, use:
46 |
47 | ```ts
48 | import * as IAP from 'react-native-iap'
49 | ...
50 |
51 | IAP.requestPurchase(...)
52 | ```
53 |
--------------------------------------------------------------------------------
/docs/docs/migrate_to_12.0.0.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Migrating to 12.0.0
3 | sidebar_label: Migrating to 12.0.0
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # Migrating to 12.0.0
11 |
12 | This migration will focus on integrating the latest store sdk for Amazon
13 |
14 | # AndroidManifest
15 |
16 | add `android:exported="true"` to `ResponseReceiver`
17 |
18 | ## The new Amazon IAP SDK needs you to include your public key.
19 |
20 | Instructions: https://developer.amazon.com/docs/in-app-purchasing/integrate-appstore-sdk.html#configure_key
21 |
22 | # Added verifyLicense method
23 |
24 | It will return a status of the app see: AmazonLicensingStatus for values returned
25 |
26 | ```ts
27 | import IapAmazon from "react-native-iap"
28 | ...
29 |
30 | const status = await IapAmazon.verifyLicense()
31 |
32 | if(status === 'LICENSED'){
33 | ...
34 | }
35 |
36 | ```
37 |
--------------------------------------------------------------------------------
/docs/docs/old-to-remove-product.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Product
3 | sidebar_label: Product
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # Product
11 |
12 | > All the following properties are `String`
13 |
14 | | Property | iOS | And | Comment |
15 | | ---------------------------------------- | :-: | :-: | -------------------------------------------------------------------------------------------------------------------------- |
16 | | `price` | ✓ | ✓ | Localized price string, with only number (eg. `1.99`). |
17 | | `productId` | ✓ | ✓ | Returns a string needed to purchase the item later. |
18 | | `currency` | ✓ | ✓ | Returns the currency code. |
19 | | `countryCode` | ✓ | | Returns the store country code. |
20 | | `localizedPrice` | ✓ | ✓ | Localized price string, with number and currency symbol (eg. `$1.99`). |
21 | | `title` | ✓ | ✓ | Returns the title Android and localizedTitle on iOS. |
22 | | `description` | ✓ | ✓ | Returns the localized description on Android and iOS. |
23 | | `introductoryPrice` | ✓ | ✓ | Formatted introductory price of a subscription, including its currency sign, such as €3.99. The price doesn't include tax. |
24 | | `introductoryPriceAsAmountIOS` | ✓ | | Localized introductory price string, with only number (eg. `0.99`). |
25 | | `introductoryPricePaymentModeIOS` | ✓ | | The payment mode for this product discount. |
26 | | `introductoryPriceNumberOfPeriods` | ✓ | | An integer that indicates the number of periods the product discount is available. |
27 | | `introductoryPriceNumberOfPeriodsIOS` | ✓ | | An integer that indicates the number of periods the product discount is available. |
28 | | `introductoryPriceSubscriptionPeriod` | ✓ | | An object that defines the period for the product discount. |
29 | | `introductoryPriceSubscriptionPeriodIOS` | ✓ | | An object that defines the period for the product discount. |
30 | | `subscriptionPeriodNumberIOS` | ✓ | | The period number (in string) of subscription period. |
31 | | `subscriptionPeriodUnitIOS` | ✓ | | The period unit in `DAY`, `WEEK`, `MONTH` or `YEAR`. |
32 |
--------------------------------------------------------------------------------
/docs/docs/support-us.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Support
3 | sidebar_label: Support
4 | ---
5 |
6 | import AdFitTopFixed from "@site/src/uis/AdFitTopFixed";
7 |
8 |
9 |
10 | # Support React Native IAP
11 |
12 | `react-native-iap` is an open source project with MIT license. We are willing to
13 | maintain this repository to support devs to monetize around the world.
14 |
15 | Since `IAP` itself is not perfect on each platform, we desperately need
16 | this project to be maintained. If you'd like to help us, please contact the [maintainer: hyo@hyo.dev](mailto:hyo@hyo.dev).
17 |
18 | ## Our Sponsors
19 |
20 | ### Gold Tier
21 |
22 | ## Sponsors
23 |
24 | ### Gold Tier
25 |
26 |
27 |
28 |
29 |
30 | ---
31 |
32 | ## Past Sponsors
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | After 7 years of maintenance, we finally have an official sponsor. I am deeply grateful. 🙇🏻♂️
42 |
--------------------------------------------------------------------------------
/docs/docusaurus.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('@docusaurus/types').Config} */
2 | const config = {
3 | title: 'React Native IAP',
4 | tagline: 'React Native In App Purchase',
5 | url: 'https://react-native-iap.hyo.dev',
6 | baseUrl: '/',
7 | favicon: 'img/favicon.ico',
8 | organizationName: 'hyochan',
9 | projectName: 'react-native-iap',
10 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */
11 | themeConfig: {
12 | colorMode: {
13 | defaultMode: 'dark',
14 | },
15 | navbar: {
16 | title: 'React Native IAP',
17 | logo: {
18 | alt: 'react native iap',
19 | src: 'img/logo.png',
20 | },
21 | items: [
22 | {
23 | href: 'https://github.com/hyochan/react-native-iap',
24 | position: 'right',
25 | className: 'header-github-link',
26 | 'aria-label': 'GitHub repository',
27 | },
28 | ],
29 | },
30 | footer: {
31 | style: 'dark',
32 | links: [
33 | {
34 | title: 'Learn',
35 | items: [
36 | {
37 | label: 'Get started',
38 | to: 'https://react-native-iap.hyo.dev/docs/get-started',
39 | },
40 | {
41 | label: 'Installation',
42 | to: 'https://react-native-iap.hyo.dev/docs/installation',
43 | },
44 | ],
45 | },
46 | {
47 | title: 'GitHub',
48 | items: [
49 | {
50 | label: 'Issues',
51 | to: 'https://github.com/hyochan/react-native-iap/issues',
52 | },
53 | {
54 | label: 'Discussions',
55 | to: 'https://github.com/hyochan/react-native-iap/discussions',
56 | },
57 | {
58 | label: 'Pull requests',
59 | to: 'https://github.com/hyochan/react-native-iap/pulls',
60 | },
61 | {
62 | label: 'Changelog',
63 | to: 'https://github.com/hyochan/react-native-iap/releases',
64 | },
65 | ],
66 | },
67 | {
68 | title: 'More',
69 | items: [
70 | {
71 | label: 'Open Collective',
72 | to: 'https://opencollective.com/react-native-iap',
73 | },
74 | ],
75 | },
76 | ],
77 | copyright: 'React Native IAP is under MIT license.',
78 | },
79 | },
80 | presets: [
81 | [
82 | '@docusaurus/preset-classic',
83 | /** @type {import('@docusaurus/preset-classic').Options} */
84 | {
85 | docs: {
86 | sidebarPath: require.resolve('./sidebars.js'),
87 | editUrl:
88 | 'https://github.com/hyochan/react-native-iap/edit/main/docs/',
89 | remarkPlugins: [
90 | [require('@docusaurus/remark-plugin-npm2yarn'), {sync: true}],
91 | ],
92 | },
93 | theme: {
94 | customCss: require.resolve('./src/css/custom.css'),
95 | },
96 | },
97 | ],
98 | ],
99 | onBrokenLinks: 'log',
100 | onBrokenMarkdownLinks: 'log',
101 | };
102 |
103 | module.exports = config;
104 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "docusaurus": "docusaurus",
7 | "start": "docusaurus start",
8 | "build": "docusaurus build",
9 | "swizzle": "docusaurus swizzle",
10 | "deploy": "docusaurus deploy",
11 | "clear": "docusaurus clear",
12 | "serve": "docusaurus serve",
13 | "write-translations": "docusaurus write-translations",
14 | "write-heading-ids": "docusaurus write-heading-ids"
15 | },
16 | "dependencies": {
17 | "@docusaurus/core": "2.4.1",
18 | "@docusaurus/preset-classic": "2.4.1",
19 | "@docusaurus/remark-plugin-npm2yarn": "2.4.1",
20 | "clsx": "1.2.1",
21 | "file-loader": "6.2.0",
22 | "react": "17.0.2",
23 | "react-dom": "17.0.2",
24 | "url-loader": "4.1.1"
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.5%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/docs/sidebars.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
3 | };
4 |
--------------------------------------------------------------------------------
/docs/src/css/custom.css:
--------------------------------------------------------------------------------
1 | /* stylelint-disable docusaurus/copyright-header */
2 |
3 | :root {
4 | --ifm-color-primary: #25c2a0;
5 | --ifm-color-primary-dark: rgb(33, 175, 144);
6 | --ifm-color-primary-darker: rgb(31, 165, 136);
7 | --ifm-color-primary-darkest: rgb(26, 136, 112);
8 | --ifm-color-primary-light: rgb(70, 203, 174);
9 | --ifm-color-primary-lighter: rgb(102, 212, 189);
10 | --ifm-color-primary-lightest: rgb(146, 224, 208);
11 | --ifm-code-font-size: 95%;
12 | }
13 |
14 | .docusaurus-highlight-code-line {
15 | background-color: rgb(72, 77, 91);
16 | display: block;
17 | margin: 0 calc(-1 * var(--ifm-pre-padding));
18 | padding: 0 var(--ifm-pre-padding);
19 | }
20 |
21 | .header-github-link:hover {
22 | opacity: 0.6;
23 | }
24 |
25 | .header-github-link::before {
26 | content: '';
27 | width: 24px;
28 | height: 24px;
29 | display: flex;
30 | background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E")
31 | no-repeat;
32 | }
33 |
34 | [data-theme='dark'] .header-github-link::before {
35 | background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E")
36 | no-repeat;
37 | }
38 |
39 | .footer__copyright {
40 | margin-top: 50px;
41 | }
42 |
--------------------------------------------------------------------------------
/docs/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useState} from 'react';
2 | import {Redirect} from 'react-router-dom';
3 | import useBaseUrl from '@docusaurus/useBaseUrl';
4 | import AdFit from '../uis/AdFit';
5 |
6 | export default function Home() {
7 | useEffect(() => {
8 | location.href = '/docs/get-started';
9 | }, []);
10 |
11 | return (
12 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/docs/src/uis/AdFit.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {useEffect} from 'react';
3 |
4 | export default function AdFit({
5 | className = 'adfit',
6 | style,
7 | unit,
8 | height,
9 | width,
10 | }) {
11 | useEffect(() => {
12 | let ins = document.createElement('ins');
13 | let scr = document.createElement('script');
14 | ins.className = 'kakao_ad_area';
15 | // @ts-ignore
16 | ins.style = 'display:none; width:100%;';
17 | // @ts-ignore
18 | scr.async = 'true';
19 | scr.type = 'text/javascript';
20 | scr.src = '//t1.daumcdn.net/kas/static/ba.min.js';
21 | ins.setAttribute('data-ad-width', width.toString());
22 | ins.setAttribute('data-ad-height', height.toString());
23 | ins.setAttribute('data-ad-unit', unit.toString());
24 | document.querySelector(`.${className}`).appendChild(ins);
25 | document.querySelector(`.${className}`).appendChild(scr);
26 | }, []);
27 |
28 | return (
29 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/docs/src/uis/AdFitTopFixed.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AdFit from './AdFit';
3 |
4 | export default function AdFitTopFixed() {
5 | return (
6 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/docs/static/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyochan/react-native-iap/a0578ecd9e349ac8959c7b3a2019dc4151dcd0a9/docs/static/.nojekyll
--------------------------------------------------------------------------------
/docs/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyochan/react-native-iap/a0578ecd9e349ac8959c7b3a2019dc4151dcd0a9/docs/static/img/favicon.ico
--------------------------------------------------------------------------------
/docs/static/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyochan/react-native-iap/a0578ecd9e349ac8959c7b3a2019dc4151dcd0a9/docs/static/img/logo.png
--------------------------------------------------------------------------------
/ios/IapTypes.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IapTypes.swift
3 | // RNIap
4 | //
5 | // Created by Andres Aguilar on 8/18/22.
6 | //
7 |
8 | import Foundation
9 | import StoreKit
10 |
11 | typealias RNIapIosPromise = (RCTPromiseResolveBlock, RCTPromiseRejectBlock)
12 |
13 | public enum StoreError: Error {
14 | case failedVerification
15 | }
16 |
17 | enum IapErrors: String, CaseIterable {
18 | case E_UNKNOWN = "E_UNKNOWN"
19 | case E_SERVICE_ERROR = "E_SERVICE_ERROR"
20 | case E_USER_CANCELLED = "E_USER_CANCELLED"
21 | case E_USER_ERROR = "E_USER_ERROR"
22 | case E_ITEM_UNAVAILABLE = "E_ITEM_UNAVAILABLE"
23 | case E_REMOTE_ERROR = "E_REMOTE_ERROR"
24 | case E_NETWORK_ERROR = "E_NETWORK_ERROR"
25 | case E_RECEIPT_FAILED = "E_RECEIPT_FAILED"
26 | case E_RECEIPT_FINISHED_FAILED = "E_RECEIPT_FINISHED_FAILED"
27 | case E_DEVELOPER_ERROR = "E_DEVELOPER_ERROR"
28 | case E_PURCHASE_ERROR = "E_PURCHASE_ERROR"
29 | case E_SYNC_ERROR = "E_SYNC_ERROR"
30 | case E_DEFERRED_PAYMENT = "E_DEFERRED_PAYMENT"
31 | case E_TRANSACTION_VALIDATION_FAILED = "E_TRANSACTION_VALIDATION_FAILED"
32 | func asInt() -> Int {
33 | return IapErrors.allCases.firstIndex(of: self)!
34 | }
35 | }
36 |
37 | // Based on https://stackoverflow.com/a/40135192/570612
38 | extension Date {
39 | var millisecondsSince1970: Int64 {
40 | return Int64((self.timeIntervalSince1970 * 1000.0).rounded())
41 | }
42 |
43 | var millisecondsSince1970String: String {
44 | return String(self.millisecondsSince1970)
45 | }
46 |
47 | init(milliseconds: Int64) {
48 | self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000)
49 | }
50 | }
51 |
52 | extension SKProductsRequest {
53 | var key: String {
54 | return String(self.hashValue)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/ios/IapUtils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IapUtils.swift
3 | // RNIap
4 | //
5 | // Created by Andres Aguilar on 8/15/22.
6 | //
7 |
8 | import Foundation
9 | import StoreKit
10 | import React
11 |
12 | public func debugMessage(_ object: Any...) {
13 | #if DEBUG
14 | for item in object {
15 | print("[react-native-iap] \(item)")
16 | }
17 | #endif
18 | }
19 | @available(iOS 15.0, tvOS 15.0, *)
20 | func checkVerified(_ result: VerificationResult) throws -> T {
21 | // Check whether the JWS passes StoreKit verification.
22 | switch result {
23 | case .unverified:
24 | // StoreKit parses the JWS, but it fails verification.
25 | throw StoreError.failedVerification
26 |
27 | case .verified(let safe):
28 | // The result is verified. Return the unwrapped value.
29 | return safe
30 | }
31 | }
32 |
33 | @available(iOS 15.0, *)
34 | func currentWindow() async -> UIWindow? {
35 | await withCheckedContinuation { continuation in
36 | DispatchQueue.main.async {
37 | continuation.resume(returning: RCTKeyWindow())
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/ios/LatestPromiseKeeper.swift:
--------------------------------------------------------------------------------
1 | import StoreKit
2 |
3 | // Only keeps latest promise, assumes older promises are not needed
4 | // Avoids racing conditions by storing latestPromise in a thread safe var
5 | // Cancels previous promises when new ones are added
6 | // Should not be used when all promises are relevant (e.g. Purchases)
7 | class LatestPromiseKeeper {
8 | private var latestPromise: ThreadSafe<(RCTPromiseResolveBlock, RCTPromiseRejectBlock)?> = ThreadSafe(nil)
9 | private var latestRequest: ThreadSafe = ThreadSafe(nil)
10 |
11 | func setLatestPromise(request: SKProductsRequest, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
12 | // Cancel the ongoing request and reject the existing promise before setting a new one
13 | cancelOngoingRequest()
14 |
15 | latestRequest.atomically { $0 = request }
16 | latestPromise.atomically { $0 = (resolve, reject) }
17 | }
18 |
19 | func cancelOngoingRequest() {
20 | latestPromise.atomically { promiseResolvers in
21 | if let (_, reject) = promiseResolvers {
22 | // Reject the promise with an error indicating that it was cancelled due to a new request
23 | reject("E_CANCELED", "Previous request was cancelled due to a new request", nil)
24 | }
25 | }
26 |
27 | latestRequest.atomically { ongoingRequest in
28 | ongoingRequest?.cancel()
29 | ongoingRequest = nil
30 | }
31 |
32 | clearLatestPromiseAndRequest()
33 | }
34 |
35 | func resolveIfRequestMatches(matchingRequest: SKProductsRequest, items: [[String: Any?]], operation: (RCTPromiseResolveBlock, [[String: Any?]]) -> Void) {
36 | latestPromise.atomically { promiseResolvers in
37 | guard let (resolve, _) = promiseResolvers else { return }
38 |
39 | latestRequest.atomically { ongoingRequest in
40 | guard ongoingRequest === matchingRequest else { return }
41 |
42 | operation(resolve, items)
43 | }
44 | }
45 | clearLatestPromiseAndRequest()
46 | }
47 |
48 | private func clearLatestPromiseAndRequest() {
49 | latestPromise.atomically { $0 = nil }
50 | latestRequest.atomically { $0 = nil }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/ios/RNIapIos-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
--------------------------------------------------------------------------------
/ios/RNIapIos.m:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | #import
4 |
5 | @interface RCT_EXTERN_MODULE (RNIapIos, NSObject)
6 |
7 | RCT_EXTERN_METHOD(disable:
8 | (RCTPromiseResolveBlock)resolve
9 | reject:(RCTPromiseRejectBlock)reject)
10 | RCT_EXTERN_METHOD(initConnection:
11 | (RCTPromiseResolveBlock)resolve
12 | reject:(RCTPromiseRejectBlock)reject)
13 |
14 | RCT_EXTERN_METHOD(endConnection:
15 | (RCTPromiseResolveBlock)resolve
16 | reject:(RCTPromiseRejectBlock)reject)
17 |
18 | RCT_EXTERN_METHOD(getItems:
19 | (NSArray*)skus
20 | resolve:(RCTPromiseResolveBlock)resolve
21 | reject:(RCTPromiseRejectBlock)reject)
22 |
23 | RCT_EXTERN_METHOD(getAvailableItems:
24 | (BOOL)automaticallyFinishRestoredTransactions
25 | resolve:(RCTPromiseResolveBlock)resolve
26 | reject:(RCTPromiseRejectBlock)reject)
27 |
28 | RCT_EXTERN_METHOD(buyProduct:
29 | (NSString*)sku
30 | andDangerouslyFinishTransactionAutomatically:(BOOL)andDangerouslyFinishTransactionAutomatically
31 | applicationUsername:(NSString*)applicationUsername
32 | quantity:(NSInteger)quantity
33 | withOffer:(NSDictionary*)discountOffer
34 | resolve:(RCTPromiseResolveBlock)resolve
35 | reject:(RCTPromiseRejectBlock)reject)
36 |
37 | RCT_EXTERN_METHOD(clearTransaction:
38 | (RCTPromiseResolveBlock)resolve
39 | reject:(RCTPromiseRejectBlock)reject)
40 |
41 | RCT_EXTERN_METHOD(clearProducts:
42 | (RCTPromiseResolveBlock)resolve
43 | reject:(RCTPromiseRejectBlock)reject)
44 |
45 | RCT_EXTERN_METHOD(promotedProduct:
46 | (RCTPromiseResolveBlock)resolve
47 | reject:(RCTPromiseRejectBlock)reject)
48 |
49 | RCT_EXTERN_METHOD(buyPromotedProduct:(RCTPromiseResolveBlock)resolve
50 | reject:(RCTPromiseRejectBlock)reject)
51 |
52 | RCT_EXTERN_METHOD(requestReceipt:
53 | (BOOL)refresh
54 | resolve:(RCTPromiseResolveBlock)resolve
55 | reject:(RCTPromiseRejectBlock)reject)
56 | RCT_EXTERN_METHOD(finishTransaction:
57 | (NSString*)transactionIdentifier
58 | resolve:(RCTPromiseResolveBlock)resolve
59 | reject:(RCTPromiseRejectBlock)reject)
60 |
61 | RCT_EXTERN_METHOD(getPendingTransactions:
62 | (RCTPromiseResolveBlock)resolve
63 | reject:(RCTPromiseRejectBlock)reject)
64 |
65 | RCT_EXTERN_METHOD(presentCodeRedemptionSheet:
66 | (RCTPromiseResolveBlock)resolve
67 | reject:(RCTPromiseRejectBlock)reject)
68 |
69 | @end
70 |
--------------------------------------------------------------------------------
/ios/RNIapIosSk2.m:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | #import
4 | #ifdef __IPHONE_15_0
5 |
6 |
7 | @interface RCT_EXTERN_MODULE (RNIapIosSk2, NSObject)
8 |
9 | RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(isAvailable){
10 | if (@available(iOS 15.0, *)) {
11 | return [NSNumber numberWithInt:1];
12 | }else{
13 | return [NSNumber numberWithInt:0];
14 | }
15 | }
16 |
17 | RCT_EXTERN_METHOD(disable:
18 | (RCTPromiseResolveBlock)resolve
19 | reject:(RCTPromiseRejectBlock)reject)
20 |
21 | RCT_EXTERN_METHOD(initConnection:
22 | (RCTPromiseResolveBlock)resolve
23 | reject:(RCTPromiseRejectBlock)reject)
24 |
25 | RCT_EXTERN_METHOD(endConnection:
26 | (RCTPromiseResolveBlock)resolve
27 | reject:(RCTPromiseRejectBlock)reject)
28 |
29 | RCT_EXTERN_METHOD(getItems:
30 | (NSArray*)skus
31 | resolve:(RCTPromiseResolveBlock)resolve
32 | reject:(RCTPromiseRejectBlock)reject)
33 |
34 | RCT_EXTERN_METHOD(getAvailableItems:
35 | (BOOL)alsoPublishToEventListener
36 | onlyIncludeActiveItems:(BOOL)onlyIncludeActiveItems
37 | resolve:(RCTPromiseResolveBlock)resolve
38 | reject:(RCTPromiseRejectBlock)reject)
39 |
40 | RCT_EXTERN_METHOD(buyProduct:
41 | (NSString*)sku
42 | andDangerouslyFinishTransactionAutomatically:(BOOL)andDangerouslyFinishTransactionAutomatically
43 | appAccountToken:(NSString*)appAccountToken
44 | quantity:(NSInteger)quantity
45 | withOffer:(NSDictionary*)withOffer
46 | resolve:(RCTPromiseResolveBlock)resolve
47 | reject:(RCTPromiseRejectBlock)reject)
48 |
49 | RCT_EXTERN_METHOD(isEligibleForIntroOffer:
50 | (NSString*)groupID
51 | resolve:(RCTPromiseResolveBlock)resolve
52 | reject:(RCTPromiseRejectBlock)reject)
53 |
54 | RCT_EXTERN_METHOD(subscriptionStatus:
55 | (NSString*)sku
56 | resolve:(RCTPromiseResolveBlock)resolve
57 | reject:(RCTPromiseRejectBlock)reject)
58 |
59 | RCT_EXTERN_METHOD(currentEntitlement:
60 | (NSString*)sku
61 | resolve:(RCTPromiseResolveBlock)resolve
62 | reject:(RCTPromiseRejectBlock)reject)
63 |
64 | RCT_EXTERN_METHOD(latestTransaction:
65 | (NSString*)sku
66 | resolve:(RCTPromiseResolveBlock)resolve
67 | reject:(RCTPromiseRejectBlock)reject)
68 |
69 | RCT_EXTERN_METHOD(finishTransaction:
70 | (NSString*)transactionIdentifier
71 | resolve:(RCTPromiseResolveBlock)resolve
72 | reject:(RCTPromiseRejectBlock)reject)
73 |
74 | RCT_EXTERN_METHOD(getPendingTransactions:
75 | (RCTPromiseResolveBlock)resolve
76 | reject:(RCTPromiseRejectBlock)reject)
77 |
78 | RCT_EXTERN_METHOD(sync:
79 | (RCTPromiseResolveBlock)resolve
80 | reject:(RCTPromiseRejectBlock)reject)
81 |
82 | RCT_EXTERN_METHOD(presentCodeRedemptionSheet:
83 | (RCTPromiseResolveBlock)resolve
84 | reject:(RCTPromiseRejectBlock)reject)
85 |
86 | RCT_EXTERN_METHOD(showManageSubscriptions:
87 | (RCTPromiseResolveBlock)resolve
88 | reject:(RCTPromiseRejectBlock)reject)
89 |
90 | RCT_EXTERN_METHOD(clearTransaction:
91 | (RCTPromiseResolveBlock)resolve
92 | reject:(RCTPromiseRejectBlock)reject)
93 |
94 | RCT_EXTERN_METHOD(beginRefundRequest:
95 | (NSString*)sku
96 | resolve:(RCTPromiseResolveBlock)resolve
97 | reject:(RCTPromiseRejectBlock)reject)
98 |
99 | RCT_EXTERN_METHOD(getStorefront:
100 | (RCTPromiseResolveBlock)resolve
101 | reject:(RCTPromiseRejectBlock)reject)
102 | @end
103 | #endif
104 |
--------------------------------------------------------------------------------
/ios/ThreadSafe.swift:
--------------------------------------------------------------------------------
1 | class ThreadSafe {
2 | private var _value: A
3 | private let queue = DispatchQueue(label: "ThreadSafe")
4 |
5 | init(_ value: A) {
6 | self._value = value
7 | }
8 |
9 | var value: A {
10 | return queue.sync { _value }
11 | }
12 |
13 | func atomically(_ transform: (inout A) -> Void) {
14 | queue.sync {
15 | transform(&self._value)
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | clearMocks: true,
3 | coverageDirectory: 'coverage',
4 | moduleDirectories: ['node_modules', 'src'],
5 | modulePathIgnorePatterns: ['IapExample', 'lib', 'fixtures'],
6 | preset: 'react-native',
7 | setupFiles: ['/test/mocks/react-native-modules.js'],
8 | transform: {
9 | '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
10 | '\\.(ts|tsx)$': 'ts-jest',
11 | },
12 | transformIgnorePatterns: ['node_modules/(?!@react-native|react-native)'],
13 | };
14 |
--------------------------------------------------------------------------------
/plugin/__tests__/withIAP-test.ts:
--------------------------------------------------------------------------------
1 | jest.mock('expo/config-plugins', () => {
2 | const plugins = jest.requireActual('expo/config-plugins');
3 |
4 | return {
5 | ...plugins,
6 | WarningAggregator: {addWarningAndroid: jest.fn()},
7 | };
8 | });
9 |
--------------------------------------------------------------------------------
/plugin/build/withIAP.d.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin } from 'expo/config-plugins';
2 | type PaymentProvider = 'Amazon AppStore' | 'both' | 'Play Store';
3 | export declare const modifyAppBuildGradle: (buildGradle: string, paymentProvider: PaymentProvider) => string;
4 | interface Props {
5 | paymentProvider?: PaymentProvider;
6 | }
7 | declare const _default: ConfigPlugin;
8 | export default _default;
9 |
--------------------------------------------------------------------------------
/plugin/build/withIAP.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.modifyAppBuildGradle = void 0;
4 | const config_plugins_1 = require("expo/config-plugins");
5 | const config_plugins_2 = require("expo/config-plugins");
6 | const pkg = require('../../package.json');
7 | const hasPaymentProviderProperValue = (paymentProvider) => {
8 | return ['Amazon AppStore', 'Play Store', 'both'].includes(paymentProvider);
9 | };
10 | const linesToAdd = {
11 | ['Amazon AppStore']: `missingDimensionStrategy "store", "amazon"`,
12 | ['Play Store']: `missingDimensionStrategy "store", "play"`,
13 | ['both']: `flavorDimensions "appstore"
14 |
15 | productFlavors {
16 | googlePlay {
17 | dimension "appstore"
18 | missingDimensionStrategy "store", "play"
19 | }
20 |
21 | amazon {
22 | dimension "appstore"
23 | missingDimensionStrategy "store", "amazon"
24 | }
25 | }`,
26 | };
27 | const addToBuildGradle = (newLine, anchor, offset, buildGradle) => {
28 | const lines = buildGradle.split('\n');
29 | const lineIndex = lines.findIndex((line) => line.match(anchor));
30 | // add after given line
31 | lines.splice(lineIndex + offset, 0, newLine);
32 | return lines.join('\n');
33 | };
34 | const modifyAppBuildGradle = (buildGradle, paymentProvider) => {
35 | if (paymentProvider === 'both') {
36 | if (buildGradle.includes(`flavorDimensions "appstore"`)) {
37 | return buildGradle;
38 | }
39 | return addToBuildGradle(linesToAdd[paymentProvider], 'defaultConfig', -1, buildGradle);
40 | }
41 | const missingDimensionStrategy = linesToAdd[paymentProvider];
42 | if (buildGradle.includes(missingDimensionStrategy)) {
43 | return buildGradle;
44 | }
45 | return addToBuildGradle(missingDimensionStrategy, 'defaultConfig', 1, buildGradle);
46 | };
47 | exports.modifyAppBuildGradle = modifyAppBuildGradle;
48 | const withIAPAndroid = (config, { paymentProvider }) => {
49 | // eslint-disable-next-line @typescript-eslint/no-shadow
50 | config = (0, config_plugins_1.withAppBuildGradle)(config, (config) => {
51 | config.modResults.contents = (0, exports.modifyAppBuildGradle)(config.modResults.contents, paymentProvider);
52 | return config;
53 | });
54 | return config;
55 | };
56 | const withIAP = (config, props) => {
57 | const paymentProvider = props?.paymentProvider ?? 'Play Store';
58 | if (!hasPaymentProviderProperValue(paymentProvider)) {
59 | config_plugins_1.WarningAggregator.addWarningAndroid('react-native-iap', `The payment provider '${paymentProvider}' is not supported. Please update your app.json file with one of the following supported values: 'Play Store', 'Amazon AppStore', or 'both'.`);
60 | return config;
61 | }
62 | try {
63 | config = withIAPAndroid(config, { paymentProvider });
64 | }
65 | catch (error) {
66 | config_plugins_1.WarningAggregator.addWarningAndroid('react-native-iap', `There was a problem configuring react-native-iap in your native Android project: ${error}`);
67 | }
68 | return config;
69 | };
70 | exports.default = (0, config_plugins_2.createRunOncePlugin)(withIAP, pkg.name, pkg.version);
71 |
--------------------------------------------------------------------------------
/plugin/jest.config.js:
--------------------------------------------------------------------------------
1 | // In documentation there is `preset: expo-module-scripts`, but it runs tests for every platform (ios, android, web, node)
2 | // We need only node tests right now
3 | module.exports = {
4 | preset: 'jest-expo/node',
5 | };
6 |
--------------------------------------------------------------------------------
/plugin/src/withIAP.ts:
--------------------------------------------------------------------------------
1 | import {WarningAggregator, withAppBuildGradle} from 'expo/config-plugins';
2 | import {ConfigPlugin, createRunOncePlugin} from 'expo/config-plugins';
3 |
4 | const pkg = require('../../package.json');
5 |
6 | type PaymentProvider = 'Amazon AppStore' | 'both' | 'Play Store';
7 |
8 | const hasPaymentProviderProperValue = (
9 | paymentProvider: string,
10 | ): paymentProvider is PaymentProvider => {
11 | return ['Amazon AppStore', 'Play Store', 'both'].includes(paymentProvider);
12 | };
13 |
14 | const linesToAdd: {[key in PaymentProvider]: string} = {
15 | ['Amazon AppStore']: `missingDimensionStrategy "store", "amazon"`,
16 | ['Play Store']: `missingDimensionStrategy "store", "play"`,
17 | ['both']: `flavorDimensions "appstore"
18 |
19 | productFlavors {
20 | googlePlay {
21 | dimension "appstore"
22 | missingDimensionStrategy "store", "play"
23 | }
24 |
25 | amazon {
26 | dimension "appstore"
27 | missingDimensionStrategy "store", "amazon"
28 | }
29 | }`,
30 | };
31 |
32 | const addToBuildGradle = (
33 | newLine: string,
34 | anchor: RegExp | string,
35 | offset: number,
36 | buildGradle: string,
37 | ) => {
38 | const lines = buildGradle.split('\n');
39 | const lineIndex = lines.findIndex((line) => line.match(anchor));
40 | // add after given line
41 | lines.splice(lineIndex + offset, 0, newLine);
42 | return lines.join('\n');
43 | };
44 |
45 | export const modifyAppBuildGradle = (
46 | buildGradle: string,
47 | paymentProvider: PaymentProvider,
48 | ) => {
49 | if (paymentProvider === 'both') {
50 | if (buildGradle.includes(`flavorDimensions "appstore"`)) {
51 | return buildGradle;
52 | }
53 | return addToBuildGradle(
54 | linesToAdd[paymentProvider],
55 | 'defaultConfig',
56 | -1,
57 | buildGradle,
58 | );
59 | }
60 |
61 | const missingDimensionStrategy = linesToAdd[paymentProvider];
62 | if (buildGradle.includes(missingDimensionStrategy)) {
63 | return buildGradle;
64 | }
65 | return addToBuildGradle(
66 | missingDimensionStrategy,
67 | 'defaultConfig',
68 | 1,
69 | buildGradle,
70 | );
71 | };
72 |
73 | const withIAPAndroid: ConfigPlugin<{paymentProvider: PaymentProvider}> = (
74 | config,
75 | {paymentProvider},
76 | ) => {
77 | // eslint-disable-next-line @typescript-eslint/no-shadow
78 | config = withAppBuildGradle(config, (config) => {
79 | config.modResults.contents = modifyAppBuildGradle(
80 | config.modResults.contents,
81 | paymentProvider,
82 | );
83 | return config;
84 | });
85 |
86 | return config;
87 | };
88 |
89 | interface Props {
90 | paymentProvider?: PaymentProvider;
91 | }
92 |
93 | const withIAP: ConfigPlugin = (config, props) => {
94 | const paymentProvider = props?.paymentProvider ?? 'Play Store';
95 |
96 | if (!hasPaymentProviderProperValue(paymentProvider)) {
97 | WarningAggregator.addWarningAndroid(
98 | 'react-native-iap',
99 |
100 | `The payment provider '${paymentProvider}' is not supported. Please update your app.json file with one of the following supported values: 'Play Store', 'Amazon AppStore', or 'both'.`,
101 | );
102 | return config;
103 | }
104 | try {
105 | config = withIAPAndroid(config, {paymentProvider});
106 | } catch (error) {
107 | WarningAggregator.addWarningAndroid(
108 | 'react-native-iap',
109 |
110 | `There was a problem configuring react-native-iap in your native Android project: ${error}`,
111 | );
112 | }
113 |
114 | return config;
115 | };
116 |
117 | export default createRunOncePlugin(withIAP, pkg.name, pkg.version);
118 |
--------------------------------------------------------------------------------
/plugin/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo-module-scripts/tsconfig.plugin",
3 | "compilerOptions": {
4 | "outDir": "build",
5 | "rootDir": "src"
6 | },
7 | "include": ["./src"],
8 | "exclude": ["**/__mocks__/*", "**/__tests__/*"]
9 | }
10 |
--------------------------------------------------------------------------------
/scripts/bootstrap.js:
--------------------------------------------------------------------------------
1 | const os = require('os');
2 | const path = require('path');
3 | const child_process = require('child_process');
4 |
5 | const root = path.resolve(__dirname, '..');
6 | const args = process.argv.slice(2);
7 | const options = {
8 | cwd: process.cwd(),
9 | env: process.env,
10 | stdio: 'inherit',
11 | encoding: 'utf-8',
12 | };
13 |
14 | if (os.type() === 'Windows_NT') {
15 | options.shell = true;
16 | }
17 |
18 | let result;
19 |
20 | if (process.cwd() !== root || args.length) {
21 | // We're not in the root of the project, or additional arguments were passed
22 | // In this case, forward the command to `yarn`
23 | result = child_process.spawnSync('yarn', args, options);
24 | } else {
25 | // If `yarn` is run without arguments, perform bootstrap
26 | result = child_process.spawnSync('yarn', ['bootstrap'], options);
27 | }
28 |
29 | process.exitCode = result.status;
30 |
--------------------------------------------------------------------------------
/src/__tests__/iap.test.ts:
--------------------------------------------------------------------------------
1 | import {NativeModules} from 'react-native';
2 |
3 | import {initConnection} from '../iap';
4 | import type {SubscriptionAndroid} from '../types';
5 | import {SubscriptionPlatform} from '../types';
6 |
7 | jest.mock(
8 | '../../node_modules/react-native/Libraries/Utilities/Platform',
9 | () => ({
10 | OS: 'android',
11 | select: (dict: {[x: string]: any}) => dict.android,
12 | }),
13 | );
14 |
15 | describe('Google Play IAP', () => {
16 | it("should call init on Google Play's native module but not on Amazon's", async () => {
17 | await initConnection();
18 | expect(NativeModules.RNIapModule.initConnection).toBeCalled();
19 | expect(NativeModules.RNIapAmazonModule.initConnection).not.toBeCalled();
20 | });
21 |
22 | it('has correct typings for an Android subscription', () => {
23 | // This actually doesn't test any logic, but just verifies the typings
24 | // are correct
25 | const subscription: SubscriptionAndroid = {
26 | platform: SubscriptionPlatform.android,
27 | // The rest of this is pasted in from a `console.log` result of an
28 | // Android getSubscriptions call from 2022-10-11.
29 | subscriptionOfferDetails: [
30 | {
31 | pricingPhases: {
32 | pricingPhaseList: [
33 | {
34 | recurrenceMode: 1,
35 | priceAmountMicros: '49990000',
36 | billingCycleCount: 0,
37 | billingPeriod: 'P1Y',
38 | priceCurrencyCode: 'USD',
39 | formattedPrice: '$49.99',
40 | },
41 | ],
42 | },
43 | offerTags: [],
44 | offerToken: 'dGVzdA==',
45 | basePlanId: 'basePlanId',
46 | offerId: 'offerId',
47 | },
48 | ],
49 | name: 'MyApp Pro: Annual Plan',
50 | productType: 'subs',
51 | description: '',
52 | title: 'MyApp Pro: Annual Plan (MyApp - Productivity App)',
53 | productId: 'app_4999_1y_1w0',
54 | };
55 |
56 | expect(subscription).toBeTruthy();
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useIAP';
2 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './iap';
2 | export * from './types';
3 | export * from './eventEmitter';
4 | export * from './hooks/useIAP';
5 | export * from './hooks/withIAPContext';
6 | export * from './purchaseError';
7 | export * from './modules';
8 |
--------------------------------------------------------------------------------
/src/internal/enhancedFetch.ts:
--------------------------------------------------------------------------------
1 | interface OverwrittenRequestInit extends Omit {
2 | body: Record;
3 | }
4 |
5 | export const enhancedFetch = async (
6 | url: string,
7 | init?: OverwrittenRequestInit,
8 | ) => {
9 | const response = await fetch(url, {
10 | method: init?.method ?? 'GET',
11 | headers: {
12 | Accept: 'application/json',
13 | 'Content-Type': 'application/json',
14 | },
15 | ...(init?.body ? {body: JSON.stringify(init.body)} : {}),
16 | });
17 |
18 | if (!response.ok) {
19 | throw Object.assign(new Error(response.statusText), {
20 | statusCode: response.status,
21 | });
22 | }
23 |
24 | return response.json() as Promise;
25 | };
26 |
--------------------------------------------------------------------------------
/src/internal/fillProductsWithAdditionalData.ts:
--------------------------------------------------------------------------------
1 | import {NativeModules} from 'react-native';
2 |
3 | import type {ProductCommon} from '../types';
4 |
5 | const {RNIapAmazonModule} = NativeModules;
6 |
7 | /**
8 | * For Amazon products, we add the currency code from the user's information
9 | * since it isn't included in the product information.
10 | */
11 | export const fillProductsWithAdditionalData = async (
12 | items: T[],
13 | ): Promise => {
14 | if (RNIapAmazonModule) {
15 | // On amazon we must get the user marketplace to detect the currency
16 | const user = await RNIapAmazonModule.getUser();
17 |
18 | const currencies = {
19 | CA: 'CAD',
20 | ES: 'EUR',
21 | AU: 'AUD',
22 | DE: 'EUR',
23 | IN: 'INR',
24 | US: 'USD',
25 | JP: 'JPY',
26 | GB: 'GBP',
27 | IT: 'EUR',
28 | BR: 'BRL',
29 | FR: 'EUR',
30 | };
31 |
32 | const currency =
33 | currencies[user.userMarketplaceAmazon as keyof typeof currencies];
34 |
35 | // Add currency to items
36 | items.forEach((item) => {
37 | if (currency) {
38 | const {originalPrice} = item;
39 | item.currency = currency;
40 | item.price = originalPrice ?? '0.0';
41 | item.localizedPrice = originalPrice ?? '0.0';
42 | }
43 | });
44 | }
45 |
46 | return items;
47 | };
48 |
--------------------------------------------------------------------------------
/src/internal/index.ts:
--------------------------------------------------------------------------------
1 | export * from './enhancedFetch';
2 | export * from './fillProductsWithAdditionalData';
3 | export * from './platform';
4 |
--------------------------------------------------------------------------------
/src/internal/platform.ts:
--------------------------------------------------------------------------------
1 | import {NativeModules, Platform} from 'react-native';
2 |
3 | import {ErrorCode} from '../purchaseError';
4 |
5 | const {RNIapIos, RNIapIosSk2, RNIapModule, RNIapAmazonModule} = NativeModules;
6 |
7 | export const isIos = Platform.OS === 'ios';
8 | export const isAndroid = Platform.OS === 'android';
9 | export const isAmazon = isAndroid && !!RNIapAmazonModule;
10 | export const isPlay = isAndroid && !!RNIapModule;
11 |
12 | // Android
13 |
14 | let androidNativeModule = RNIapModule;
15 |
16 | export const setAndroidNativeModule = (
17 | nativeModule: typeof RNIapModule,
18 | ): void => {
19 | androidNativeModule = nativeModule;
20 | };
21 |
22 | export const checkNativeAndroidAvailable = (): void => {
23 | if (!RNIapModule && !RNIapAmazonModule) {
24 | throw new Error(ErrorCode.E_IAP_NOT_AVAILABLE);
25 | }
26 | };
27 |
28 | /**
29 | * If changing the typings of `getAndroidModule` to accommodate extra modules,
30 | * make sure to update `getAndroidModuleType`.
31 | */
32 | export const getAndroidModule = ():
33 | | typeof RNIapModule
34 | | typeof RNIapAmazonModule => {
35 | checkNativeAndroidAvailable();
36 |
37 | return androidNativeModule
38 | ? androidNativeModule
39 | : RNIapModule
40 | ? RNIapModule
41 | : RNIapAmazonModule;
42 | };
43 |
44 | /**
45 | * Returns whether the Android in-app-purchase code is using the Android,
46 | * Amazon, or another store.
47 | */
48 | export const getAndroidModuleType = (): 'android' | 'amazon' | null => {
49 | const module = getAndroidModule();
50 | switch (module) {
51 | case RNIapModule:
52 | return 'android';
53 | case RNIapAmazonModule:
54 | return 'amazon';
55 | default:
56 | return null;
57 | }
58 | };
59 |
60 | export const getNativeModule = ():
61 | | typeof RNIapModule
62 | | typeof RNIapAmazonModule
63 | | typeof RNIapIos
64 | | typeof RNIapIosSk2 => {
65 | return isAndroid ? getAndroidModule() : getIosModule();
66 | };
67 |
68 | // iOS
69 |
70 | let iosNativeModule: typeof RNIapIos | typeof RNIapIosSk2 = RNIapIos;
71 |
72 | export const isStorekit2Available = (): boolean =>
73 | isIos && RNIapIosSk2?.isAvailable() === 1;
74 |
75 | export const isIosStorekit2 = () =>
76 | isIos &&
77 | !!iosNativeModule &&
78 | iosNativeModule === RNIapIosSk2 &&
79 | isStorekit2Available();
80 |
81 | export const setIosNativeModule = (
82 | nativeModule: typeof RNIapIos | typeof RNIapIosSk2,
83 | ): void => {
84 | iosNativeModule = nativeModule;
85 | };
86 |
87 | export const storekit2Mode = () => {
88 | iosNativeModule = RNIapIosSk2;
89 | if (isStorekit2Available()) {
90 | RNIapIos.disable();
91 | return true;
92 | }
93 | if (isIos) {
94 | console.warn('Storekit 2 is not available on this device');
95 | return false;
96 | }
97 | return true;
98 | };
99 |
100 | export const storekit1Mode = () => {
101 | iosNativeModule = RNIapIos;
102 | if (isStorekit2Available()) {
103 | RNIapIosSk2.disable();
104 | return true;
105 | }
106 | return false;
107 | };
108 |
109 | export const storekitHybridMode = () => {
110 | if (isStorekit2Available()) {
111 | iosNativeModule = RNIapIosSk2;
112 | console.info('Using Storekit 2');
113 | return true;
114 | } else {
115 | iosNativeModule = RNIapIos;
116 | console.info('Using Storekit 1');
117 | return true;
118 | }
119 | };
120 |
121 | const checkNativeIOSAvailable = (): void => {
122 | if (!RNIapIos && !isStorekit2Available()) {
123 | throw new Error(ErrorCode.E_IAP_NOT_AVAILABLE);
124 | }
125 | };
126 |
127 | export const getIosModule = (): typeof RNIapIos | typeof RNIapIosSk2 => {
128 | checkNativeIOSAvailable();
129 |
130 | return iosNativeModule
131 | ? iosNativeModule
132 | : RNIapIosSk2
133 | ? RNIapIosSk2
134 | : RNIapIos;
135 | };
136 |
--------------------------------------------------------------------------------
/src/modules/amazon.ts:
--------------------------------------------------------------------------------
1 | import {NativeModules} from 'react-native';
2 |
3 | import {enhancedFetch} from '../internal';
4 | import type {Product, Purchase, Sku} from '../types';
5 | import type {
6 | AmazonLicensingStatus,
7 | ReceiptType,
8 | UserDataAmazon,
9 | } from '../types/amazon';
10 |
11 | import type {NativeModuleProps} from './common';
12 | // ----------
13 |
14 | type GetUser = () => Promise;
15 | type FlushFailedPurchasesCachedAsPending = () => Promise;
16 | type GetItemsByType = (type: string, skus: Sku[]) => Promise;
17 | type GetAvailableItems = () => Promise;
18 | type BuyItemByType = (sku: Sku) => Promise;
19 |
20 | type AcknowledgePurchase = (
21 | purchaseToken: string,
22 | developerPayloadAndroid?: string,
23 | ) => Promise;
24 |
25 | type ConsumeProduct = (
26 | purchaseToken: string,
27 | developerPayloadAndroid?: string,
28 | ) => Promise;
29 |
30 | type StartListening = () => Promise;
31 |
32 | export interface AmazonModuleProps extends NativeModuleProps {
33 | getUser: GetUser;
34 | flushFailedPurchasesCachedAsPending: FlushFailedPurchasesCachedAsPending;
35 | getItemsByType: GetItemsByType;
36 | getAvailableItems: GetAvailableItems;
37 | buyItemByType: BuyItemByType;
38 | acknowledgePurchase: AcknowledgePurchase;
39 | consumeProduct: ConsumeProduct;
40 | /** @deprecated to be renamed to sendUnconsumedPurchases if not removed completely */
41 | startListening: StartListening;
42 | verifyLicense: () => Promise;
43 | deepLinkToSubscriptions: (isAmazonDevice: boolean) => Promise;
44 | }
45 |
46 | export const AmazonModule =
47 | NativeModules.RNIapAmazonModule as AmazonModuleProps;
48 |
49 | /**
50 | * Validate receipt for Amazon. NOTE: This method is here for debugging purposes only. Including
51 | * your developer secret in the binary you ship to users is potentially dangerous.
52 | * Use server side validation instead for your production builds
53 | * @param {string} developerSecret: from the Amazon developer console.
54 | * @param {string} userId who purchased the item.
55 | * @param {string} receiptId long obfuscated string returned when purchasing the item
56 | * @param {boolean} useSandbox Defaults to true, use sandbox environment or production.
57 | * @returns {Promise