Skip to content

Commit

Permalink
feat: [2.1.0] Add expo compatibility plugin (#11)
Browse files Browse the repository at this point in the history
* feat: add expo plugin

* chore: version change

---------
  • Loading branch information
msasinowski authored May 7, 2024
1 parent e3fc6bb commit bf9f7af
Show file tree
Hide file tree
Showing 7 changed files with 2,928 additions and 79 deletions.
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,40 @@ https://developer.paypal.com/braintree/docs/start/overview

| React Native Paypal reborn Version | Braintree Android SDK | Braintree IOS SDK | Minimum SDK Android | Minimum SDK IOS |
| :--------------------------------: | :-------------------: | :---------------: | :-----------------: | :-------------: |
| 0.0.1 | v3.x | v5.x | 21 | 13.0 |
| 0.0.1 | v3.x | v5.x | 21 | 13.0 |
| 0.1.0 | v3.x | v5.x | 21 | 13.0 |
| 1.0.0 | v4.2.x | v5.x | 21 | 13.0 |
| 1.1.0 | v4.41.x | v5.x | 21 | 13.0 |
| 2.0.0 | v4.41.x | v6.17.0 | 21 | 14.0 |
| 2.0.1 | v4.41.x | v6.17.0 | 21 | 14.0 |
| 2.1.0 | v4.41.x | v6.17.0 | 21 | 14.0 |

## Integration
### Expo Based Project (expo SDK 50) (Alpha)
From version 2.1.0 of the package, react-native-paypal-reborn added a possibility to use the package into expo based project, without need to eject from the expo. Special expo plugin was added into the source of the package which can be used. in any expo project.

Expo based project needs minimum integration from the app perspective.
In Your `app.config.ts` or `app.config.json` or `app.config.js` please add react-native-paypal-reborn plugin into plugins section.
```javascript
...
plugins: [
[
"react-native-paypal-reborn",
{
xCodeProjectAppName: "xCodeProjectAppName",
},
],
...
```
`xCodeProjectAppName` - Name of your xCode project in case of example app in this repository it will be `PaypalRebornExample`

#### Android Specific
Currently expo-plugin written for making changes into Android settings files, using non danger modifiers from expo-config-plugins

#### iOS Specific
Currently expo-plugin written for making changes into IOS settings files, using one danger modifier from expo-config-plugins called `withAppDelegate`
### React Native Bare Project (react-native-cli)

## Android Specific
#### Android Specific

In Your `AndroidManifest.xml`, `android:allowBackup="false"` can be replaced `android:allowBackup="true"`, it is responsible for app backup.

Expand All @@ -39,7 +64,7 @@ Also, add this intent-filter to your main activity in `AndroidManifest.xml`

```

## iOS Specific
#### iOS Specific
```bash
cd ios
pod install
Expand Down
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('./lib/commonjs/plugin/withReactNativePaypalReborn');
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"android",
"ios",
"cpp",
"app.plugin.js",
"*.podspec",
"!ios/build",
"!android/build",
Expand Down Expand Up @@ -55,15 +56,18 @@
"devDependencies": {
"@commitlint/config-conventional": "^17.0.2",
"@evilmartians/lefthook": "^1.5.0",
"@expo/config-plugins": "7.9.1",
"@react-native/eslint-config": "^0.73.1",
"@release-it/conventional-changelog": "^5.0.0",
"@types/jest": "^29.5.5",
"@types/react": "^18.2.44",
"commitlint": "^17.0.2",
"del-cli": "^5.1.0",
"eol": "^0.9.1",
"eslint": "^8.51.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"expo": "^50.0.17",
"jest": "^29.7.0",
"prettier": "^3.0.3",
"react": "18.2.0",
Expand All @@ -77,9 +81,16 @@
"@types/react": "^18.2.44"
},
"peerDependencies": {
"eol": "*",
"expo": ">=50.0.0",
"react": "*",
"react-native": "*"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
}
},
"workspaces": [
"example"
],
Expand Down
128 changes: 128 additions & 0 deletions src/plugin/withReactNativePaypalReborn.android.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {
withAndroidManifest,
AndroidConfig,
type ConfigPlugin,
} from '@expo/config-plugins';

const { getMainActivityOrThrow } = AndroidConfig.Manifest;

export const withReactNativePaypalRebornAndroid: ConfigPlugin = (
expoConfig
) => {
return withAndroidManifest(expoConfig, (config) => {
config.modResults = addPaypalIntentFilter(config.modResults);
return config;
});
};

type ManifestData = {
$: {
[key: string]: string | undefined;
'android:host'?: string;
'android:pathPrefix'?: string;
'android:scheme'?: string;
};
};

// Add new intent filter
// <activity>
// ...
// <intent-filter>
// <action android:name="android.intent.action.VIEW" />
// <category android:name="android.intent.category.DEFAULT" />
// <category android:name="android.intent.category.BROWSABLE" />
// <data android:scheme="${applicationId}.braintree" />
// </intent-filter>
// </activity>;
const intentActionView = 'android.intent.action.VIEW';
const intentCategoryDefault = 'android.intent.category.DEFAULT';
const intentCategoryBrowsable = 'android.intent.category.BROWSABLE';
const intentDataBraintree = '${applicationId}.braintree';

export const addPaypalIntentFilter = (
modResults: AndroidConfig.Manifest.AndroidManifest
): AndroidConfig.Manifest.AndroidManifest => {
const mainActivity = getMainActivityOrThrow(modResults);
// We want always to add the data to the first intent filter
const intentFilters = mainActivity['intent-filter'];
if (!intentFilters?.length) {
console.warn(
'withReactNativePaypalRebornAndroid.addPaypalIntentFilter: No .Intent Filters'
);
return modResults;
}
const {
isIntentActionExist,
isIntentCategoryBrowsableExist,
isIntentCategoryDefaultExist,
isIntentDataBraintreeExist,
} = checkAndroidManifestData(intentFilters);

if (
isIntentActionExist &&
isIntentCategoryBrowsableExist &&
isIntentCategoryDefaultExist &&
isIntentDataBraintreeExist
) {
console.warn(
'withReactNativePaypalRebornAndroid: AndroidManifest not require any changes'
);
return modResults;
}
intentFilters.push({
action: [
{
$: { 'android:name': intentActionView },
},
],
category: [
{ $: { 'android:name': intentCategoryDefault } },
{ $: { 'android:name': intentCategoryBrowsable } },
],
data: [{ $: { 'android:scheme': '${applicationId}.braintree' } }],
});
return modResults;
};

const checkAndroidManifestData = (
intentFilters: AndroidConfig.Manifest.ManifestIntentFilter[]
) => ({
isIntentActionExist: isElementInAndroidManifestExist(
intentFilters,
intentActionView,
'action'
),
isIntentCategoryDefaultExist: isElementInAndroidManifestExist(
intentFilters,
intentCategoryDefault,
'category'
),
isIntentCategoryBrowsableExist: isElementInAndroidManifestExist(
intentFilters,
intentCategoryBrowsable,
'category'
),
isIntentDataBraintreeExist: isElementInAndroidManifestExist(
intentFilters,
intentDataBraintree,
'data'
),
});

const isElementInAndroidManifestExist = (
intentFilters: AndroidConfig.Manifest.ManifestIntentFilter[] | undefined,
value: string,
type: 'action' | 'data' | 'category'
) =>
!!intentFilters?.some((intentFilter) =>
intentFilter[type]?.find((item) => {
switch (type) {
case 'action':
case 'category':
return item.$['android:name'] === value;
case 'data':
const typedItem = item as ManifestData;
return typedItem.$['android:scheme'] === value;
}
})
);
141 changes: 141 additions & 0 deletions src/plugin/withReactNativePaypalReborn.ios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/* eslint-disable no-bitwise */
import {
withAppDelegate,
withInfoPlist,
type ConfigPlugin,
IOSConfig,
} from '@expo/config-plugins';
import eol from 'eol';
import type { ReactNativePaypalRebornPluginProps } from './withReactNativePaypalReborn';

export const withReactNativePaypalRebornAppDelegate: ConfigPlugin<
ReactNativePaypalRebornPluginProps
> = (expoConfig, { xCodeProjectAppName }) => {
return withAppDelegate(expoConfig, (config) => {
const appDelegate = config.modResults;
let contents = eol.split(appDelegate.contents);
// Step 1 Edit Import part
// Editing import part for -swift.h file to be able to use Braintree
const importSwiftHeaderFileContent = `#import "${xCodeProjectAppName}-Swift.h"`;
const importSwiftHeaderFileIndex = contents.findIndex((content) =>
content.includes(importSwiftHeaderFileContent)
);
// If importSwiftHeaderFileContent do not exist in AppDelegate.mm
if (!~importSwiftHeaderFileIndex) {
contents = [importSwiftHeaderFileContent, ...contents];
}
const importExpoModulesSwiftHeader = `#import "ExpoModulesCore-Swift.h"`;
const importExpoModulesSwiftHeaderFileIndex = contents.findIndex(
(content) => content.includes(importExpoModulesSwiftHeader)
);
// If importExpoModulesSwiftHeader do not exist in AppDelegate.mm
if (!~importExpoModulesSwiftHeaderFileIndex) {
contents = [importExpoModulesSwiftHeader, ...contents];
}
// Step 2 Add configure method in didFinishLaunchingWithOptions
const didFinishLaunchingWithOptions = 'didFinishLaunchingWithOptions';
const payPalRebornConfigureLine = ' [PaypalRebornConfig configure];';
let didFinishLaunchingWithOptionsElementIndex = contents.findIndex(
(content) => content.includes(didFinishLaunchingWithOptions)
);
const payPalRebornConfigureLineIndex = contents.findIndex((content) =>
content.includes(payPalRebornConfigureLine)
);
// If didFinishLaunchingWithOptions exist in AppDelegate.mm and payPalRebornConfigureLine do not exist
if (
!~payPalRebornConfigureLineIndex &&
!!~didFinishLaunchingWithOptionsElementIndex
) {
contents.splice(
// We are adding +2 to the index to insert content after '{' block
didFinishLaunchingWithOptionsElementIndex + 2,
0,
payPalRebornConfigureLine
);
}
// Step 3 Add method to properly handle openUrl method in AppDelegate.m
const openUrlMethod =
'- (BOOL)application:(UIApplication *)application openURL';
const payPalRebornOpenUrlLines = [
' if ([url.scheme localizedCaseInsensitiveCompare:[PaypalRebornConfig getPaymentUrlScheme]] == NSOrderedSame) {',
' return [PaypalRebornConfig handleUrl:url];',
' }',
];
const openUrlMethodElementIndex = contents.findIndex((content) =>
content.includes(openUrlMethod)
);
const payPalRebornOpenUrlLineIndex = contents.findIndex((content) =>
content.includes(payPalRebornOpenUrlLines?.[0] ?? '')
);
// If openUrlMethodElementIndex exist in AppDelegate.mm and payPalRebornOpenUrlLineIndex do not exist
if (!~payPalRebornOpenUrlLineIndex && !!~openUrlMethodElementIndex) {
contents.splice(
// We are adding +1 to the index to insert content after '{' block
openUrlMethodElementIndex + 1,
0,
...payPalRebornOpenUrlLines
);
}
config.modResults.contents = contents.join('\n');
return config;
});
};

/**
* Add a new wrapper Swift file to the Xcode project for Swift compatibility.
*/
export const withSwiftPaypalRebornWrapperFile: ConfigPlugin = (config) => {
return IOSConfig.XcodeProjectFile.withBuildSourceFile(config, {
filePath: 'PaypalRebornConfig.swift',
contents: [
'import Braintree',
'import Foundation',
'',
'@objc public class PaypalRebornConfig: NSObject {',
'',
'@objc(configure)',
'public static func configure() {',
' BTAppContextSwitcher.sharedInstance.returnURLScheme = self.getPaymentUrlScheme()',
'}',
'',
'@objc(getPaymentUrlScheme)',
'public static func getPaymentUrlScheme() -> String {',
' let bundleIdentifier = Bundle.main.bundleIdentifier ?? ""',
' return bundleIdentifier + ".braintree"',
'}',
'',
'@objc(handleUrl:)',
'public static func handleUrl(url: URL) -> Bool {',
' return BTAppContextSwitcher.sharedInstance.handleOpen(url)',
'}',
'}',
].join('\n'),
});
};

export const withReactNativePaypalRebornPlist: ConfigPlugin = (expoConfig) => {
return withInfoPlist(expoConfig, (config) => {
const bundleIdentifier = config.ios?.bundleIdentifier ?? '';
const bundleIdentifierWithBraintreeSchema = `${bundleIdentifier}.braintree`;
const bundleUrlTypes = config.modResults.CFBundleURLTypes;
const isBraintreeSchemaNotExist = !bundleUrlTypes?.find((urlTypes) => {
urlTypes.CFBundleURLSchemes.includes(bundleIdentifierWithBraintreeSchema);
});
// If Braintree url schema for specific bundle id not exist then add this entry
if (isBraintreeSchemaNotExist) {
config.modResults.CFBundleURLTypes = bundleUrlTypes?.map(
(bundleUrlType) => {
const isUrlSchemaContainBundleIdentifier =
bundleUrlType.CFBundleURLSchemes.includes(bundleIdentifier);
if (isUrlSchemaContainBundleIdentifier) {
bundleUrlType.CFBundleURLSchemes.push(
bundleIdentifierWithBraintreeSchema
);
}
return bundleUrlType;
}
);
}
return config;
});
};
Loading

0 comments on commit bf9f7af

Please sign in to comment.