Skip to content
Open
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"react-native-webview": "^13.16.0"
},
"devDependencies": {
"@expo/config-plugins": "^9.0.16",
"@expo/config-plugins": "^54.0.3",
"@react-native/babel-preset": "^0.81.0",
"@react-native/eslint-config": "^0.81.1",
"@types/jest": "^29.5.14",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package {{package}}
import {{package}}.MainApplication

import android.app.Activity
import android.content.Intent
import android.os.Bundle

class LaunchActivity : Activity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val application = application as MainApplication

// check that MainActivity is not started yet
if (!application.isActivityInBackStack(MainActivity::class.java)) {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
}

finish()
}
}
186 changes: 186 additions & 0 deletions src/plugin/with-android-launch-activity/withAndroidLaunchActivity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import {
AndroidConfig,
ConfigPlugin,
withAndroidManifest,
withDangerousMod,
withMainActivity,
withMainApplication,
CodeGenerator,
} from '@expo/config-plugins';
import fs from 'node:fs';
import path from 'node:path';

const LAUNCH_ACTIVITY_TEMPLATE = fs.readFileSync(
path.join(__dirname, 'LaunchActivity.kt.template'),
'utf-8'
);

export const withAndroidLaunchActivity: ConfigPlugin<{
addLaunchActivity?: boolean;
}> = (expoConfig, { addLaunchActivity = false }) => {
if (!addLaunchActivity) return expoConfig;
const androidPackage = expoConfig.android?.package;
if (!androidPackage) return expoConfig;

let resultConfig = withDangerousMod(expoConfig, [
'android',
async (config) => {
const appSrcDir = path.join(
config.modRequest.platformProjectRoot,
'app/src/main/java/',
...config.android!.package!.split('.')
);
const filePath = path.join(appSrcDir, 'LaunchActivity.kt');
fs.mkdirSync(appSrcDir, { recursive: true });
const fileContent = LAUNCH_ACTIVITY_TEMPLATE.replace(
/{{package}}/g,
config.android!.package!
);
fs.writeFileSync(filePath, fileContent);
return config;
},
]);

resultConfig = withAndroidManifest(resultConfig, (config) => {
const mainApplication = config?.modResults?.manifest?.application?.[0];

if (!mainApplication) throw new Error('MainApplication missing!');
if (!mainApplication.activity)
throw new Error('MainApplication has no activities!');

const mainActivityIndex = mainApplication.activity?.findIndex(
(activity) => activity.$['android:name'] === '.MainActivity'
);
if (mainActivityIndex === -1) throw new Error('Missing MainActivity!');

const mainActivity = mainApplication.activity[mainActivityIndex];
removeIntentFilter(mainActivity, 'android.intent.category.LAUNCHER');
ensureLaunchActivity(mainApplication);

return config;
});

resultConfig = withMainApplication(resultConfig, (config) => {
const { modResults } = config;

if (
config.modResults.contents.includes(
'@generated begin @stripe/stripe-react-native'
)
)
return config;

const merged = CodeGenerator.mergeContents({
src: modResults.contents,
comment: ' //',
tag: '@stripe/stripe-react-native',
offset: 0,
anchor: / {2}override val reactNativeHost:/,
newSrc: ` private val runningActivities = ArrayList<Class<*>>()

fun addActivityToStack(cls: Class<*>) {
if (!runningActivities.contains(cls)) {
runningActivities.add(cls)
}
}

fun removeActivityFromStack(cls: Class<*>) {
runningActivities.remove(cls)
}

fun isActivityInBackStack(cls: Class<*>): Boolean {
return runningActivities.contains(cls)
}`,
});
config.modResults.contents = merged.contents;

return config;
});

resultConfig = withMainActivity(resultConfig, (config) => {
if (
config.modResults.contents.includes(
'@generated begin @stripe/stripe-react-native'
)
)
return config;
const { modResults } = config;
const { language } = modResults;

if (
config.modResults.contents.includes(
'@generated begin @stripe/stripe-react-native'
)
)
return config;

const withImports = AndroidConfig.CodeMod.addImports(
modResults.contents,
[`${androidPackage}.MainApplication`],
language === 'java'
);

const merged = CodeGenerator.mergeContents({
src: withImports,
comment: ' //',
tag: '@stripe/stripe-react-native',
offset: 1,
anchor: /super\.onCreate\(null\)/,
newSrc: `\
val app = application as MainApplication
app.addActivityToStack(this::class.java)
}

override fun onDestroy() {
super.onDestroy()
val app = application as MainApplication
app.removeActivityFromStack(this::class.java)
`,
});

config.modResults.contents = merged.contents;
return config;
});

return resultConfig;
};

/** Removes all intent-filters with an action matching the specified name */
function removeIntentFilter(
activity: AndroidConfig.Manifest.ManifestActivity,
intentFilterCategoryName: string
) {
if (!activity['intent-filter']) return;
activity['intent-filter'] = activity['intent-filter'].filter(
(filter) =>
!filter.category?.find(
(category) => category.$['android:name'] === intentFilterCategoryName
)
);
}

function ensureLaunchActivity(
application: AndroidConfig.Manifest.ManifestApplication
) {
application.activity ??= [];
const activities = application.activity;
const hasLaunchActivity = activities.some(
(activity) => activity.$['android:name'] === '.LaunchActivity'
);
if (hasLaunchActivity) return;

activities.push({
'$': {
'android:exported': 'true',
'android:name': '.LaunchActivity',
},
'intent-filter': [
{
action: [{ $: { 'android:name': 'android.intent.action.MAIN' } }],
category: [
{ $: { 'android:name': 'android.intent.category.LAUNCHER' } },
],
},
],
});
}
12 changes: 12 additions & 0 deletions src/plugin/withStripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
withPodfile,
} from '@expo/config-plugins';
import path from 'path';
import { withAndroidLaunchActivity } from './with-android-launch-activity/withAndroidLaunchActivity';

const {
addMetaDataItemToMainApplication,
Expand All @@ -32,12 +33,23 @@ type StripePluginProps = {
* Defaults to false.
*/
includeOnramp?: boolean;
/**
* If true, an extra activity will be added to the AndroidManifest.xml,
* to support re-opening your app during ongoing/finished 3DS2 verification.
* See [docs/android-chrome-tab-closes-on-background.md](docs/android-chrome-tab-closes-on-background.md)
* for more details.
*
*
* Defaults to false.
*/
addLaunchActivity?: boolean;
};

const withStripe: ConfigPlugin<StripePluginProps> = (config, props) => {
config = withStripeIos(config, props);
config = withNoopSwiftFile(config);
config = withStripeAndroid(config, props);
config = withAndroidLaunchActivity(config, props);
return config;
};

Expand Down
Loading