UNPKG

react-native-expo-braintree

Version:

React native and expo wrapper around braintree sdk fro android and ios

365 lines (341 loc) 9.95 kB
import { AndroidConfig, type ConfigPlugin, withAndroidManifest, withMainActivity, withProjectBuildGradle, WarningAggregator, } from '@expo/config-plugins'; import type { ManifestIntentFilter, StringBoolean, } from '@expo/config-plugins/build/android/Manifest'; import { addImports } from '@expo/config-plugins/build/android/codeMod'; import { mergeContents, createGeneratedHeaderComment, type MergeResults, removeGeneratedContents, } from '@expo/config-plugins/build/utils/generateCode'; interface WithExpoBraintreeAndroidProps { host: string; pathPrefix?: string; initialize3DSecure?: 'true' | 'false'; initializeGooglePay?: 'true' | 'false'; addFallbackUrlScheme?: 'true' | 'false'; } const { getMainActivityOrThrow } = AndroidConfig.Manifest; export const withExpoBraintreeAndroid: ConfigPlugin< WithExpoBraintreeAndroidProps > = ( expoConfig, { host, pathPrefix, addFallbackUrlScheme, initialize3DSecure, initializeGooglePay, } ) => { let newConfig = withAndroidManifest(expoConfig, (config) => { config.modResults = addBraintreeLinks( config.modResults, host, pathPrefix, addFallbackUrlScheme, initialize3DSecure ); return config; }); newConfig = withMainActivity(expoConfig, (config) => { const { modResults } = config; const { language } = modResults; const withImports = addImports( modResults.contents, ['com.expobraintree.ExpoBraintreeModule'], language === 'java' ); const newSrc = [ ` ExpoBraintreeModule.init()${language === 'java' ? ';' : ''}`, ]; if (initialize3DSecure === 'true') { newSrc.push( ` ExpoBraintreeModule.initThreeDSecure(this)${language === 'java' ? ';' : ''}` ); } if (initializeGooglePay === 'true') { newSrc.push( ` ExpoBraintreeModule.initGooglePay(this)${language === 'java' ? ';' : ''}` ); } const withInit = mergeContents({ src: withImports, comment: ' // add BraintreeModule import', tag: 'braintree-module-init', offset: 1, anchor: /(?<=^.*super\.onCreate.*$)/m, newSrc: newSrc.join('\n'), }); return { ...config, modResults: { ...modResults, contents: withInit.contents, }, }; }); return newConfig; }; // Add new intent filter for App Links // <activity> // ... // <intent-filter android:autoVerify="true"> // <action android:name="android.intent.action.VIEW" /> // <category android:name="android.intent.category.DEFAULT" /> // <category android:name="android.intent.category.BROWSABLE" /> // <data android:scheme="http" /> // <data android:scheme="https" /> // <data android:host="myownpersonaldomain.com" /> // <data android:pathPrefix="/braintree-payments"/> // </intent-filter> // </activity>; // If you provide a fallbackUrlScheme it will also add a new intent filter for that // <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>; export const addBraintreeLinks = ( modResults: AndroidConfig.Manifest.AndroidManifest, host: string, pathPrefix?: string, addFallbackUrlScheme?: 'true' | 'false', initialize3DSecure?: 'true' | 'false' ): AndroidConfig.Manifest.AndroidManifest => { const mainActivity = getMainActivityOrThrow(modResults); const intentFilters = mainActivity['intent-filter']; // Host is required props for a plugin if (!host) { WarningAggregator.addWarningAndroid( 'withExpoBraintree addBraintreeLinks', `No Host provided for withExpoBraintree.android addBraintreeLinks` ); } // Check if the intent filter already exists if (hasIntentFilter(intentFilters, host, pathPrefix)) { WarningAggregator.addWarningAndroid( 'withExpoBraintree addBraintreeLinks', `withExpoBraintreeAndroid: AndroidManifest not require any changes` ); return modResults; } const newIntentFilter: ManifestIntentFilter = { action: [ { $: { 'android:name': 'android.intent.action.VIEW', }, }, ], category: [ { $: { 'android:name': 'android.intent.category.DEFAULT', }, }, { $: { 'android:name': 'android.intent.category.BROWSABLE', }, }, ], data: [ { $: { 'android:scheme': 'http', }, }, { $: { 'android:scheme': 'https', }, }, { $: { 'android:host': host, }, }, ], $: { 'android:autoVerify': 'true' as StringBoolean, }, }; // If there is pathPrefix then we add that if (pathPrefix) { newIntentFilter.data?.push({ $: { 'android:pathPrefix': pathPrefix, }, }); } const newFallbackUrlSchemeIntentFilter: ManifestIntentFilter = { action: [ { $: { 'android:name': 'android.intent.action.VIEW', }, }, ], category: [ { $: { 'android:name': 'android.intent.category.DEFAULT', }, }, { $: { 'android:name': 'android.intent.category.BROWSABLE', }, }, ], data: [ { $: { 'android:scheme': '${applicationId}.braintree', }, }, ], }; // Add the intent-filter to the main activity mainActivity['intent-filter'] = [ ...(mainActivity['intent-filter'] || []), newIntentFilter, ]; // If there is fallbackUrlScheme then we add that if (initialize3DSecure === 'true' || addFallbackUrlScheme === 'true') { // Add the intent-filter to the main activity mainActivity['intent-filter'] = [ ...(mainActivity['intent-filter'] || []), newFallbackUrlSchemeIntentFilter, ]; } return modResults; }; /** * Check if an intent-filter with the same data already exists * @param {object} intentFilters - The AndroidManifest intent filters * @param {string} host - The host to check * @param {string} pathPrefix - The pathPrefix to check * @returns {boolean} - Returns true if a matching intent filter is found */ function hasIntentFilter( intentFilters: AndroidConfig.Manifest.ManifestIntentFilter[] | undefined, host: string, pathPrefix?: string ) { return intentFilters?.some((filter) => { const hasAutoVerify = filter.$ && filter.$['android:autoVerify'] === 'true'; const hasViewAction = filter.action?.some( (action) => action.$['android:name'] === 'android.intent.action.VIEW' ); const hasDefaultCategory = filter.category?.some( (category) => category.$['android:name'] === 'android.intent.category.DEFAULT' ); const hasBrowsableCategory = filter.category?.some( (category) => category.$['android:name'] === 'android.intent.category.BROWSABLE' ); const hasHttpScheme = filter.data?.some( (data) => data.$['android:scheme'] === 'http' ); const hasHttpsScheme = filter.data?.some( (data) => data.$['android:scheme'] === 'https' ); const hasMatchingHost = filter.data?.some( (data) => data.$['android:host'] === host ); const hasMatchingPathPrefix = filter.data?.some( (data) => data.$['android:pathPrefix'] === pathPrefix ); // Check all conditions to ensure it matches the intent filter we want to add return ( hasAutoVerify && hasViewAction && hasDefaultCategory && hasBrowsableCategory && hasHttpScheme && hasHttpsScheme && hasMatchingHost && (hasMatchingPathPrefix || !pathPrefix) ); }); } // Because we need the package to be added AFTER the React and Google maven packages, we create a new all projects. // It's ok to have multiple all projects.repositories, so we create a new one since it's cheaper than tokenizing // the existing block to find the correct place to insert our content. const gradle3DSecureBraintreeRepo = [ `allprojects {`, ` repositories {`, ` maven {`, ` url "https://cardinalcommerceprod.jfrog.io/artifactory/android"`, ` credentials {`, ` username 'braintree_team_sdk'`, ` password 'AKCp8jQcoDy2hxSWhDAUQKXLDPDx6NYRkqrgFLRc3qDrayg6rrCbJpsKKyMwaykVL8FWusJpp'`, ` }`, ` }`, ` }`, `}`, ].join('\n'); export const withExpoBraintreeAndroidGradle: ConfigPlugin = (expoConfig) => { return withProjectBuildGradle(expoConfig, (config) => { if (config.modResults.language === 'groovy') { config.modResults.contents = appendContents({ tag: 'expo-braintree-import', src: config.modResults.contents, newSrc: gradle3DSecureBraintreeRepo, comment: '//', }).contents; } else { throw new Error( 'Cannot add expo-braintree-import maven gradle because the build.gradle is not groovy' ); } return config; }); }; export const appendContents = ({ src, newSrc, tag, comment, }: { src: string; newSrc: string; tag: string; comment: string; }): MergeResults => { const header = createGeneratedHeaderComment(newSrc, tag, comment); if (!src.includes(header)) { // Ensure the old generated contents are removed. const sanitizedTarget = removeGeneratedContents(src, tag); const contentsToAdd = [ // @something header, // contents newSrc, // @end `${comment} @generated end ${tag}`, ].join('\n'); return { contents: sanitizedTarget ?? src + contentsToAdd, didMerge: true, didClear: !!sanitizedTarget, }; } return { contents: src, didClear: false, didMerge: false }; };