Skip to content

feat: add expo config plugin #2218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,7 @@ lib/
!**/.yarn/versions

# Docs
docs/docs/api/*
docs/docs/api/*

# Keep expo plugin build code
!plugin/build
3 changes: 3 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ docs/
.gitattributes
.yarn/cache
package/
plugin/src
plugin/jest.config.js
plugin/tsconfig.json
1 change: 1 addition & 0 deletions app.plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./plugin/build/withIAP');
40 changes: 38 additions & 2 deletions docs/docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,44 @@

### How do I use `react-native-iap` in Expo?

- You should detach from `expo` and get `expokit` out of it.
- Releated issue in [#174](https://github.com/dooboolab/react-native-iap/issues/174).
> This package cannot be used in the "Expo Go" app because [it requires custom native code](https://docs.expo.io/workflow/customizing/).

After installing this npm package, add the [config plugin](https://docs.expo.io/guides/config-plugins/) to the [`plugins`](https://docs.expo.io/versions/latest/config/app/#plugins) array of your `app.json` or `app.config.js`:

```json
{
"expo": {
"plugins": ["react-native-iap"]
}
}
```

Next, rebuild your app as described in the ["Adding custom native code"](https://docs.expo.io/workflow/customizing/) guide.

## API

The plugin provides props for extra customization. Every time you change the props or plugins, you'll need to rebuild (and `prebuild`) the native app. If no extra properties are added, **Play Store** configuration will be added.

Optional prop:

- `paymentProvider` (_string_): payment provider to configure: `Play Store`, `Amazon AppStore`, `both`

#### Example

```json
{
"expo": {
"plugins": [
[
"react-native-iap",
{
"paymentProvider": "both"
}
]
]
}
}
```

### How do I handle promoted products in iOS?

Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module.exports = {
clearMocks: true,
coverageDirectory: 'coverage',
moduleDirectories: ['node_modules', 'src'],
modulePathIgnorePatterns: ['IapExample', 'lib'],
modulePathIgnorePatterns: ['IapExample', 'lib', 'fixtures'],
preset: 'react-native',
setupFiles: ['<rootDir>/test/mocks/react-native-modules.js'],
setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'],
Expand Down
11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"!**/__mocks__"
],
"scripts": {
"prepare": "bob build",
"prepare": "bob build && npm run clean:plugin && npm run build:plugin",
"release": "release-it",
"example": "yarn --cwd IapExample",
"test": "jest",
Expand All @@ -67,7 +67,10 @@
"lint:swift": "swiftlint lint --fix --format --path ios/*.swift --config .swiftlint.yml",
"format": "git ls-files -m | xargs yarn prettier --write --ignore-unknown --no-error-on-unmatched-pattern",
"bootstrap": "yarn example && yarn && yarn example pods",
"gen:doc": "typedoc"
"gen:doc": "typedoc",
"build:plugin": "tsc --build plugin",
"clean:plugin": "expo-module clean plugin",
"lint:plugin": "eslint plugin/src/*"
},
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
Expand All @@ -89,6 +92,7 @@
"eslint-plugin-jest": "26.8.2",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-simple-import-sort": "7.0.0",
"expo-module-scripts": "^3.0.4",
"jest": "28.1.3",
"monolinter": "1.0.4",
"pod-install": "0.1.38",
Expand All @@ -108,5 +112,8 @@
"peerDependencies": {
"react": ">=16.13.1",
"react-native": ">=0.65.1"
},
"dependencies": {
"@expo/config-plugins": "^5.0.4"
}
}
164 changes: 164 additions & 0 deletions plugin/__tests__/fixtures/buildGradleFiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
const appBuildGradleWithoutIAP = `
apply plugin: "com.android.application"

import com.android.build.OutputFile

def reactNativeArchitectures() {
def value = project.getProperties().get("reactNativeArchitectures")
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}

android {
ndkVersion rootProject.ext.ndkVersion

compileSdkVersion rootProject.ext.compileSdkVersion

defaultConfig {
applicationId 'com.test.withIAP'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 34
versionName "1.16.2"
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()`;

const appBuildGradleWithPlayStoreIAP = `
apply plugin: "com.android.application"

import com.android.build.OutputFile

def reactNativeArchitectures() {
def value = project.getProperties().get("reactNativeArchitectures")
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}

android {
ndkVersion rootProject.ext.ndkVersion

compileSdkVersion rootProject.ext.compileSdkVersion

defaultConfig {
missingDimensionStrategy "store", "play"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like these variables have some duplication, any thoughts on using a common template?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that there is too much of it. In current unit tests implementation, we are passing initial build.gradle (part of it to be precise) to the function and comparing the result with the correct version. This is done 3 times, one for each paymentProvider. We could remove irrelevant lines and leave only lines close to defaultConfig. What do you think?

applicationId 'com.test.withIAP'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 34
versionName "1.16.2"
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()`;

const appBuildGradleWithAmazonStoreIAP = `
apply plugin: "com.android.application"

import com.android.build.OutputFile

def reactNativeArchitectures() {
def value = project.getProperties().get("reactNativeArchitectures")
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}

android {
ndkVersion rootProject.ext.ndkVersion

compileSdkVersion rootProject.ext.compileSdkVersion

defaultConfig {
missingDimensionStrategy "store", "amazon"
applicationId 'com.test.withIAP'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 34
versionName "1.16.2"
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()`;

const appBuildGradleWithBothIAP = `
apply plugin: "com.android.application"

import com.android.build.OutputFile

def reactNativeArchitectures() {
def value = project.getProperties().get("reactNativeArchitectures")
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}

android {
ndkVersion rootProject.ext.ndkVersion

compileSdkVersion rootProject.ext.compileSdkVersion
flavorDimensions "appstore"

productFlavors {
googlePlay {
dimension "appstore"
missingDimensionStrategy "store", "play"
}

amazon {
dimension "appstore"
missingDimensionStrategy "store", "amazon"
}
}

defaultConfig {
applicationId 'com.test.withIAP'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should these be customizable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only a part of some faked build.gradle file to ensure that properties are added correctly in tests. Content is irrelevant. Maybe we should add a comment that explains it?

minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 34
versionName "1.16.2"
Comment on lines +104 to +105
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these arbitrary/ do they matter to the person building an expo app?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All files in the fixtures directory are used solely for testing and will only be utilized by a developer writing tests.

buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()`;

const projectBuildGradleWithoutIAP = `
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
ext {
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '31.0.0'
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '21')
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '31')
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '31')
if (findProperty('android.kotlinVersion')) {
kotlinVersion = findProperty('android.kotlinVersion')
}
frescoVersion = findProperty('expo.frescoVersion') ?: '2.5.0'

if (System.properties['os.arch'] == 'aarch64') {
// For M1 Users we need to use the NDK 24 which added support for aarch64
ndkVersion = '24.0.8215888'
} else {
// Otherwise we default to the side-by-side NDK version from AGP.
ndkVersion = '21.4.7075529'
}
}
}`;

const projectBuildGradleWithIAP = `
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
ext {
supportLibVersion = "28.0.0"
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '31.0.0'
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '21')
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '31')
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '31')
if (findProperty('android.kotlinVersion')) {
kotlinVersion = findProperty('android.kotlinVersion')
}
frescoVersion = findProperty('expo.frescoVersion') ?: '2.5.0'

if (System.properties['os.arch'] == 'aarch64') {
// For M1 Users we need to use the NDK 24 which added support for aarch64
ndkVersion = '24.0.8215888'
} else {
// Otherwise we default to the side-by-side NDK version from AGP.
ndkVersion = '21.4.7075529'
}
}
}`;

export {
appBuildGradleWithAmazonStoreIAP,
appBuildGradleWithBothIAP,
appBuildGradleWithoutIAP,
appBuildGradleWithPlayStoreIAP,
projectBuildGradleWithIAP,
projectBuildGradleWithoutIAP,
};
56 changes: 56 additions & 0 deletions plugin/__tests__/withIAP-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {modifyAppBuildGradle, modifyProjectBuildGradle} from '../src/withIAP';

import {
appBuildGradleWithAmazonStoreIAP,
appBuildGradleWithBothIAP,
appBuildGradleWithoutIAP,
appBuildGradleWithPlayStoreIAP,
projectBuildGradleWithIAP,
projectBuildGradleWithoutIAP,
} from './fixtures/buildGradleFiles';

jest.mock('@expo/config-plugins', () => {
const plugins = jest.requireActual('@expo/config-plugins');

return {
...plugins,
WarningAggregator: {addWarningAndroid: jest.fn()},
};
});

describe('Configures Android native project correctly', () => {
it(`Add supportLibVersion to android/build.gradle if it is not present`, () => {
expect(modifyProjectBuildGradle(projectBuildGradleWithoutIAP)).toMatch(
projectBuildGradleWithIAP,
);
});

it(`Add play store missingDimenstionStrategy to android/app/build.gradle if is not present`, () => {
expect(
modifyAppBuildGradle(appBuildGradleWithoutIAP, 'Play Store'),
).toMatch(appBuildGradleWithPlayStoreIAP);
});

it(`Add amazon store missingDimenstionStrategy to android/app/build.gradle if is not present`, () => {
expect(
modifyAppBuildGradle(appBuildGradleWithoutIAP, 'Amazon AppStore'),
).toMatch(appBuildGradleWithAmazonStoreIAP);
});

it(`Add play store and amazon payment providers to android/app/build.gradle if is not present`, () => {
expect(modifyAppBuildGradle(appBuildGradleWithoutIAP, 'both')).toMatch(
appBuildGradleWithBothIAP,
);
});
it(`Doesn't modify android/build.gradle if supportLibVersion already configured`, () => {
expect(modifyProjectBuildGradle(projectBuildGradleWithIAP)).toMatch(
projectBuildGradleWithIAP,
);
});

it(`Doesn't modify android/app/build.gradle if missingDimensionStrategy already configured`, () => {
expect(
modifyAppBuildGradle(appBuildGradleWithPlayStoreIAP, 'Play Store'),
).toMatch(appBuildGradleWithPlayStoreIAP);
});
});
12 changes: 12 additions & 0 deletions plugin/build/withIAP.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type {ConfigPlugin} from '@expo/config-plugins';
declare type PaymentProvider = 'Amazon AppStore' | 'both' | 'Play Store';
export declare const modifyAppBuildGradle: (
buildGradle: string,
paymentProvider: PaymentProvider,
) => string;
export declare const modifyProjectBuildGradle: (buildGradle: string) => string;
interface Props {
paymentProvider?: PaymentProvider;
}
declare const withIAP: ConfigPlugin<Props | undefined>;
export default withIAP;
Loading