Skip to content

Instantly share code, notes, and snippets.

@giautm
Last active October 4, 2023 14:01
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save giautm/f6ff2d44ed9d47d36dbc689f0a710e54 to your computer and use it in GitHub Desktop.
Save giautm/f6ff2d44ed9d47d36dbc689f0a710e54 to your computer and use it in GitHub Desktop.
Expo plugin to config react-native-branch
const {
AndroidConfig,
WarningAggregator,
withAndroidManifest,
withAppDelegate,
withDangerousMod,
withInfoPlist,
withMainActivity,
withPlugins,
} = require('@expo/config-plugins')
const {
createGeneratedHeaderComment,
mergeContents,
removeGeneratedContents,
} = require('@expo/config-plugins/build/utils/generateCode')
const fs = require('fs')
const path = require('path')
async function readFileAsync(path) {
return fs.promises.readFile(path, 'utf8')
}
async function saveFileAsync(path, content) {
return fs.promises.writeFile(path, content, 'utf8')
}
// Fork of config-plugins mergeContents, but appends the contents to the end of the file.
function appendContents({ src, newSrc, tag, comment }) {
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 }
}
function addBranchAppDelegateImport(src) {
const newSrc = ['#import <RNBranch/RNBranch.h>']
return mergeContents({
tag: 'rn-branch-import',
src,
newSrc: newSrc.join('\n'),
anchor: /#import "AppDelegate\.h"/,
offset: 1,
comment: '//',
})
}
// Match against `UMModuleRegistryAdapter` (unimodules), and React Native without unimodules (Expo Modules).
const MATCH_INIT = /(?:(self\.|_)(\w+)\s?=\s?\[\[UMModuleRegistryAdapter alloc\])|(?:RCTBridge\s?\*\s?(\w+)\s?=\s?\[\[RCTBridge alloc\])/g
function addBranchAppDelegateInit(src) {
const newSrc = []
newSrc.push(
' // [RNBranch useTestInstance];',
' [RNBranch initSessionWithLaunchOptions:launchOptions isReferrable:YES];',
)
return mergeContents({
tag: 'rn-branch-init',
src,
newSrc: newSrc.join('\n'),
anchor: MATCH_INIT,
offset: 0,
comment: '//',
})
}
function addBranchAppDelegateOpenURL(src) {
const newSrc = [
' if ([RNBranch application:application openURL:url options:options]) {',
' // do other deep link routing for the Facebook SDK, Pinterest SDK, etc',
' }',
]
return mergeContents({
tag: 'rn-branch-open-url',
src,
newSrc: newSrc.join('\n'),
anchor: /\(UIApplication \*\)application openURL:/,
offset: 1,
comment: '//',
})
}
function addBranchAppDelegateContinueUserActivity(src) {
const newSrc = [
' if ([RNBranch continueUserActivity:userActivity]) {',
' return YES;',
' }',
]
return mergeContents({
tag: 'rn-branch-continue-user-activity',
src,
newSrc: newSrc.join('\n'),
anchor: /\(UIApplication \*\)application continueUserActivity:/,
offset: 1,
comment: '//',
})
}
// Starting with iOS
function withBranchIos(config, data) {
// Ensure object exist
if (!config.ios) {
config.ios = {}
}
// Update the infoPlist with the branch key and branch domain
config = withInfoPlist(config, (config) => {
config.modResults.branch_app_domain = data.branch_app_domain
config.modResults.branch_key = data.branch_key
return config
})
// Update the AppDelegate.m
config = withAppDelegate(config, (config) => {
config.modResults.contents = addBranchAppDelegateImport(
config.modResults.contents,
).contents
config.modResults.contents = addBranchAppDelegateInit(
config.modResults.contents,
).contents
config.modResults.contents = addBranchAppDelegateOpenURL(
config.modResults.contents,
).contents
config.modResults.contents = addBranchAppDelegateContinueUserActivity(
config.modResults.contents,
).contents
return config
})
return config
}
async function editMainApplication(config, action) {
const mainApplicationPath = path.join(
config.modRequest.platformProjectRoot,
'app',
'src',
'main',
'java',
...config.android?.package?.split('.'),
'MainApplication.java',
)
try {
const mainApplication = action(await readFileAsync(mainApplicationPath))
return await saveFileAsync(mainApplicationPath, mainApplication)
} catch (e) {
WarningAggregator.addWarningAndroid(
'rn-branch-plugin',
`Couldn't modify MainApplication.java - ${e}.`,
)
}
}
async function editProguardRules(config, action) {
const proguardRulesPath = path.join(
config.modRequest.platformProjectRoot,
'app',
'proguard-rules.pro',
)
try {
const proguardRules = action(await readFileAsync(proguardRulesPath))
return await saveFileAsync(proguardRulesPath, proguardRules)
} catch (e) {
WarningAggregator.addWarningAndroid(
'rn-branch-plugin',
`Couldn't modify proguard-rules.pro - ${e}.`,
)
}
}
function addBranchMainApplicationImport(src, packageId) {
const newSrc = ['import io.branch.rnbranch.RNBranchModule;']
return mergeContents({
tag: 'rn-branch-import',
src,
newSrc: newSrc.join('\n'),
anchor: `package ${packageId};`,
offset: 1,
comment: '//',
})
}
function addBranchGetAutoInstance(src) {
const newSrc = [' RNBranchModule.getAutoInstance(this);']
return mergeContents({
tag: 'rn-branch-auto-instance',
src,
newSrc: newSrc.join('\n'),
anchor: /super\.onCreate\(\);/,
offset: 1,
comment: '//',
})
}
function addBranchMainActivityImport(src, packageId) {
const newSrc = [
'import android.content.Intent;',
'import io.branch.rnbranch.*;',
]
return mergeContents({
tag: 'rn-branch-import',
src,
newSrc: newSrc.join('\n'),
anchor: `package ${packageId};`,
offset: 1,
comment: '//',
})
}
function addBranchInitSession(src) {
const tag = 'rn-branch-init-session'
try {
const newSrc = [
' RNBranchModule.initSession(getIntent().getData(), this);',
]
return mergeContents({
tag,
src,
newSrc: newSrc.join('\n'),
anchor: /super\.onStart\(\);/,
offset: 1,
comment: '//',
})
} catch (err) {
if (err.code !== 'ERR_NO_MATCH') {
throw err
}
}
const newSrc = [
' @Override',
' protected void onStart() {',
' super.onStart();',
' RNBranchModule.initSession(getIntent().getData(), this);',
' }',
]
return mergeContents({
tag,
src,
newSrc: newSrc.join('\n'),
anchor: `getMainComponentName`,
offset: 3,
comment: '//',
})
}
function addBranchOnNewIntent(src) {
const tag = 'rn-branch-on-new-intent'
try {
const newSrc = [' RNBranchModule.onNewIntent(intent);']
return mergeContents({
tag,
src,
newSrc: newSrc.join('\n'),
anchor: /super\.onNewIntent\(intent\);/,
offset: 1,
comment: '//',
})
} catch (err) {
if (err.code !== 'ERR_NO_MATCH') {
throw err
}
}
const newSrc = [
' @Override',
' public void onNewIntent(Intent intent) {',
' super.onNewIntent(intent);',
' RNBranchModule.onNewIntent(intent);',
' }',
]
return mergeContents({
tag,
src,
newSrc: newSrc.join('\n'),
anchor: `getMainComponentName`,
offset: 3,
comment: '//',
})
}
const {
addMetaDataItemToMainApplication,
getMainApplicationOrThrow,
} = AndroidConfig.Manifest
// Splitting this function out of the mod makes it easier to test.
async function setCustomConfigAsync(config, androidManifest, data) {
// Get the <application /> tag and assert if it doesn't exist.
const mainApplication = getMainApplicationOrThrow(androidManifest)
addMetaDataItemToMainApplication(
mainApplication,
// value for `android:name`
'io.branch.sdk.BranchKey',
// value for `android:value`
data.branch_key,
)
return androidManifest
}
function withBranchAndroid(config, data) {
// Insert the branch_key into the AndroidManifest
config = withAndroidManifest(config, async (config) => {
// Modifiers can be async, but try to keep them fast.
config.modResults = await setCustomConfigAsync(
config,
config.modResults,
data,
)
return config
})
// Directly edit MainApplication.java
config = withDangerousMod(config, [
'android',
async (config) => {
await editMainApplication(config, (mainApplication) => {
mainApplication = addBranchMainApplicationImport(
mainApplication,
config.android?.package,
).contents
mainApplication = addBranchGetAutoInstance(mainApplication).contents
return mainApplication
})
return config
},
])
// Update proguard rules directly
config = withDangerousMod(config, [
'android',
async (config) => {
await editProguardRules(config, (proguardRules) => {
return appendContents({
tag: 'rn-branch-dont-warn',
src: proguardRules,
newSrc: ['-dontwarn io.branch.**'].join('\n'),
comment: '#',
}).contents
})
return config
},
])
// Insert the required Branch code into MainActivity.java
config = withMainActivity(config, (config) => {
config.modResults.contents = addBranchMainActivityImport(
config.modResults.contents,
config.android?.package,
).contents
config.modResults.contents = addBranchInitSession(
config.modResults.contents,
).contents
config.modResults.contents = addBranchOnNewIntent(
config.modResults.contents,
).contents
return config
})
return config
}
module.exports = (config, data) =>
withPlugins(config, [
[withBranchIos, data],
[withBranchAndroid, data],
])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment