diff --git a/package.json b/package.json index 26665c7a4..1f9819b42 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/plugin/with-android-launch-activity/LaunchActivity.kt.template b/src/plugin/with-android-launch-activity/LaunchActivity.kt.template new file mode 100644 index 000000000..e42149ba0 --- /dev/null +++ b/src/plugin/with-android-launch-activity/LaunchActivity.kt.template @@ -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() + } +} diff --git a/src/plugin/with-android-launch-activity/withAndroidLaunchActivity.ts b/src/plugin/with-android-launch-activity/withAndroidLaunchActivity.ts new file mode 100644 index 000000000..c84a3cb4c --- /dev/null +++ b/src/plugin/with-android-launch-activity/withAndroidLaunchActivity.ts @@ -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>() + + 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' } }, + ], + }, + ], + }); +} diff --git a/src/plugin/withStripe.ts b/src/plugin/withStripe.ts index 29f92f34c..9cee63b13 100644 --- a/src/plugin/withStripe.ts +++ b/src/plugin/withStripe.ts @@ -9,6 +9,7 @@ import { withPodfile, } from '@expo/config-plugins'; import path from 'path'; +import { withAndroidLaunchActivity } from './with-android-launch-activity/withAndroidLaunchActivity'; const { addMetaDataItemToMainApplication, @@ -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 = (config, props) => { config = withStripeIos(config, props); config = withNoopSwiftFile(config); config = withStripeAndroid(config, props); + config = withAndroidLaunchActivity(config, props); return config; }; diff --git a/yarn.lock b/yarn.lock index f8525a715..825648979 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1132,19 +1132,19 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== -"@expo/config-plugins@^9.0.16": - version "9.1.7" - resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-9.1.7.tgz#cbaaf883c57cd236f157f26c03784adbe6ebf9ac" - integrity sha512-8dJzOesaQS+8XuT49pdSHej1z6XG3x2fqN2O3v807ri8uhxm2N9P6+iZBn19xv9+7OxraOc2tH3nEIWE19Za0w== - dependencies: - "@expo/config-types" "^53.0.0" - "@expo/json-file" "~9.1.3" - "@expo/plist" "^0.3.3" +"@expo/config-plugins@^54.0.3": + version "54.0.3" + resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-54.0.3.tgz#2b9ffd68a48e3b51299cdbe3ee777b9f5163fc03" + integrity sha512-tBIUZIxLQfCu5jmqTO+UOeeDUGIB0BbK6xTMkPRObAXRQeTLPPfokZRCo818d2owd+Bcmq1wBaDz0VY3g+glfw== + dependencies: + "@expo/config-types" "^54.0.9" + "@expo/json-file" "~10.0.7" + "@expo/plist" "^0.4.7" "@expo/sdk-runtime-versions" "^1.0.0" chalk "^4.1.2" debug "^4.3.5" - getenv "^1.0.0" - glob "^10.4.2" + getenv "^2.0.0" + glob "^13.0.0" resolve-from "^5.0.0" semver "^7.5.4" slash "^3.0.0" @@ -1152,23 +1152,23 @@ xcode "^3.0.1" xml2js "0.6.0" -"@expo/config-types@^53.0.0": - version "53.0.5" - resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-53.0.5.tgz#bba7e0712c2c5b1d8963348d68ea96339f858db4" - integrity sha512-kqZ0w44E+HEGBjy+Lpyn0BVL5UANg/tmNixxaRMLS6nf37YsDrLk2VMAmeKMMk5CKG0NmOdVv3ngeUjRQMsy9g== +"@expo/config-types@^54.0.9": + version "54.0.9" + resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-54.0.9.tgz#b9279c47fe249b774fbd3358b6abddea08f1bcec" + integrity sha512-Llf4jwcrAnrxgE5WCdAOxtMf8FGwS4Sk0SSgI0NnIaSyCnmOCAm80GPFvsK778Oj19Ub4tSyzdqufPyeQPksWw== -"@expo/json-file@~9.1.3": - version "9.1.5" - resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-9.1.5.tgz#7d7b2dc4990dc2c2de69a571191aba984b7fb7ed" - integrity sha512-prWBhLUlmcQtvN6Y7BpW2k9zXGd3ySa3R6rAguMJkp1z22nunLN64KYTUWfijFlprFoxm9r2VNnGkcbndAlgKA== +"@expo/json-file@~10.0.7": + version "10.0.8" + resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-10.0.8.tgz#05e524d1ecc0011db0a6d66b525ea2f58cfe6d43" + integrity sha512-9LOTh1PgKizD1VXfGQ88LtDH0lRwq9lsTb4aichWTWSWqy3Ugfkhfm3BhzBIkJJfQQ5iJu3m/BoRlEIjoCGcnQ== dependencies: "@babel/code-frame" "~7.10.4" json5 "^2.2.3" -"@expo/plist@^0.3.3": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.3.5.tgz#11913c64951936101529cb26d7260ef16970fc31" - integrity sha512-9RYVU1iGyCJ7vWfg3e7c/NVyMFs8wbl+dMWZphtFtsqyN9zppGREU3ctlD3i8KUE0sCUTVnLjCWr+VeUIDep2g== +"@expo/plist@^0.4.7": + version "0.4.8" + resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.4.8.tgz#e014511a4a5008cf2b832b91caa8e9f2704127cc" + integrity sha512-pfNtErGGzzRwHP+5+RqswzPDKkZrx+Cli0mzjQaus1ZWFsog5ibL+nVT3NcporW51o8ggnt7x813vtRbPiyOrQ== dependencies: "@xmldom/xmldom" "^0.8.8" base64-js "^1.2.3" @@ -1207,6 +1207,18 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@isaacs/balanced-match@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29" + integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== + +"@isaacs/brace-expansion@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3" + integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA== + dependencies: + "@isaacs/balanced-match" "^4.0.1" + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -3518,10 +3530,10 @@ get-symbol-description@^1.1.0: es-errors "^1.3.0" get-intrinsic "^1.2.6" -getenv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/getenv/-/getenv-1.0.0.tgz#874f2e7544fbca53c7a4738f37de8605c3fcfc31" - integrity sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg== +getenv@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/getenv/-/getenv-2.0.0.tgz#b1698c7b0f29588f4577d06c42c73a5b475c69e0" + integrity sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ== glob-parent@^5.1.2: version "5.1.2" @@ -3537,7 +3549,7 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@^10.4.2, glob@^10.5.0: +glob@^10.5.0: version "10.5.0" resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c" integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== @@ -3549,6 +3561,15 @@ glob@^10.4.2, glob@^10.5.0: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" +glob@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.0.tgz#9d9233a4a274fc28ef7adce5508b7ef6237a1be3" + integrity sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA== + dependencies: + minimatch "^10.1.1" + minipass "^7.1.2" + path-scurry "^2.0.0" + glob@^7.1.1, glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -3786,7 +3807,7 @@ internal-slot@^1.1.0: hasown "^2.0.2" side-channel "^1.1.0" -invariant@^2.2.4: +invariant@2.2.4, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -4673,6 +4694,11 @@ lru-cache@^10.2.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== +lru-cache@^11.0.0: + version "11.2.4" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.4.tgz#ecb523ebb0e6f4d837c807ad1abaea8e0619770d" + integrity sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -4969,6 +4995,13 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +minimatch@^10.1.1: + version "10.1.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.1.1.tgz#e6e61b9b0c1dcab116b5a7d1458e8b6ae9e73a55" + integrity sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ== + dependencies: + "@isaacs/brace-expansion" "^5.0.0" + minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -5256,6 +5289,14 @@ path-scurry@^1.11.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +path-scurry@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.1.tgz#4b6572376cfd8b811fca9cd1f5c24b3cbac0fe10" + integrity sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA== + dependencies: + lru-cache "^11.0.0" + minipass "^7.1.2" + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -5441,6 +5482,14 @@ react-native-monorepo-config@^0.1.8: escape-string-regexp "^5.0.0" fast-glob "^3.3.3" +react-native-webview@^13.16.0: + version "13.16.0" + resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-13.16.0.tgz#c995148f944a7eaf12389f0e6d5c6f5e6a775686" + integrity sha512-Nh13xKZWW35C0dbOskD7OX01nQQavOzHbCw9XoZmar4eXCo7AvrYJ0jlUfRVVIJzqINxHlpECYLdmAdFsl9xDA== + dependencies: + escape-string-regexp "^4.0.0" + invariant "2.2.4" + react-native@^0.81.0: version "0.81.5" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.81.5.tgz#6c963f137d3979b22aef2d8482067775c8fe2fed"