Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Create Expo managed workflow plugin #288

Closed
wants to merge 5 commits into from

Conversation

anthlasserre
Copy link
Contributor

@anthlasserre anthlasserre commented Oct 5, 2023

Summary

Today @adyen/react-native is not supported by Expo.

This pull request aims to create an Expo configuration plugin for the library, enabling the community to incorporate it into Expo-managed workflow.

Tested scenarios

  1. Run expo prebuild --clean in the example-expo app
  2. Ensure the native configuration has been well implemented by looking at:
  • ./example-expo/ios/ExampleExpo/AppDelegate.mm
  • ./example-expo/android/app/src/main/java/com/onebasketdemo/expo/MainActivity.java
  • ./example-expo/android/app/src/main/AndroidManifest.xml
  1. Local build through XCode & Android Studio
  2. Development and development of client builds through EAS

Fixed issue: #108

@descorp
Copy link
Contributor

descorp commented Oct 6, 2023

Great thanks @anthlasserre 💚

We will verify this from our side.

@anthlasserre
Copy link
Contributor Author

Any updates there @descorp ?

@descorp
Copy link
Contributor

descorp commented Oct 20, 2023

Hey @anthlasserre

I had attempted to run the Expo locally and verify this branch, but my attempt wasn't successful 😅

We are working on the 1.2.0 release; hopefully after that, we are going to address this PR.
Sorry for keeping you waiting 💚

@anthlasserre
Copy link
Contributor Author

Hey @anthlasserre

I had attempted to run the Expo locally and verify this branch, but my attempt wasn't successful 😅

We are working on the 1.2.0 release; hopefully after that, we are going to address this PR. Sorry for keeping you waiting 💚

@descorp Hey mate, no worries feel free to reach me if you need some help on your Expo project.

@gunnartorfis
Copy link
Contributor

I'd also be happy to help out in order to get this released 💯

@Seamus1989
Copy link

just did something similar for use with the drop in components. Tested on ios + android, just popping here to see if it helps in anyway!

const {
  withAppDelegate,
  withAppBuildGradle,
  withAndroidManifest,
  AndroidConfig,
  withMainActivity,
} = require('expo/config-plugins');

const androidMultilineFileChanges = {
  buildGradle: `defaultConfig {\n        manifestPlaceholders = [redirectScheme: "adyenreactnative"],`,
  mainActivityImports: `import android.os.Bundle;\nimport com.adyenreactnativesdk.AdyenCheckout;\nimport android.content.Intent;`,
  mainActivityCode: `super.onCreate(null);\n      AdyenCheckout.setLauncherActivity(this);\n\n     }\n    @Override\n        public void onNewIntent(Intent intent) {\n            super.onNewIntent(intent);\n            AdyenCheckout.handleIntent(intent);\n    }\n    @Override\n    public void onActivityResult(int requestCode, int resultCode, Intent data) {\n      super.onActivityResult(requestCode, resultCode, data);\n      AdyenCheckout.handleActivityResult(requestCode, resultCode, data);`,
};

const iosMultiLineFileChanges = {
  appDelegateImports:
    '#import "AppDelegate.h"\n#import <adyen-react-native/ADYRedirectComponent.h>',
  appDelegateCode: `return [super application:application didFinishLaunchingWithOptions:launchOptions];\n  [AdyenCheckout setAppService:[AdyenCheckoutService new]];`,
};

// IOS
const generateIosAppDelegate = (cnfg) => {
  // Edit AppDelegate.m with contents needed for Adyen.
  return withAppDelegate(cnfg, (config) => {
    const appDelegate = config.modResults;
    appDelegate.contents = appDelegate.contents.replace(
      `#import "AppDelegate.h"`,
      iosMultiLineFileChanges.appDelegateImports
    );
    appDelegate.contents = appDelegate.contents.replace(
      'return [super application:application didFinishLaunchingWithOptions:launchOptions];',
      iosMultiLineFileChanges.appDelegateCode
    );

    return config;
  });
};

// ANDROID
const generateAndroidIntentFilter = () => {
  return {
    action: [{ $: { 'android:name': 'android.intent.action.VIEW' } }],
    category: [
      { $: { 'android:name': 'android.intent.category.DEFAULT' } },
      { $: { 'android:name': 'android.intent.category.BROWSABLE' } },
    ],
    data: [
      {
        $: {
          'android:scheme': '${redirectScheme}',
          'android:host': '${applicationId}',
        },
      },
    ],
  };
};

const generateAndroidApplicationService = () => {
  return {
    $: {
      'android:name':
        'com.adyenreactnativesdk.component.dropin.AdyenCheckoutService',
      'android:exported': 'false',
    },
  };
};

const validateAndroidManifestPreConfigure = (cnfg) => {
  const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(
    cnfg.modResults
  );
  if (!mainApplication.activity.length) {
    throw new Error('MainActivity not found');
  }
  const mainActivityInd = mainApplication.activity.findIndex((item) => {
    return item.$['android:name'] === '.MainActivity';
  });
  if (mainActivityInd === -1) {
    throw new Error('MainActivity not found');
  }
  return { mainActivityInd };
};

const generateAndroidManifest = (config) => {
  return withAndroidManifest(config, (cnfg) => {
    // Edit AndroidManifest.xml with contents needed for Adyen
    const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(
      cnfg.modResults
    );
    const { mainActivityInd } = validateAndroidManifestPreConfigure(cnfg);

    mainApplication.activity[mainActivityInd]['intent-filter'].push(
      generateAndroidIntentFilter()
    );

    mainApplication.service = generateAndroidApplicationService();

    return cnfg;
  });
};

const generateAndroidAppBuildGradle = (cnfg) => {
  // Edit app/build.gradle with contents needed for Adyen
  return withAppBuildGradle(cnfg, (config) => {
    /**
     * NOTE - TODO test and fix this.
     * From Adyen/react-native "buildscript {" we see rootProject.ext.adyenReactNativeRedirectScheme = "adyenreactnative"
     * if using manifestPlaceholders = [redirectScheme: rootProject.ext.adyenReactNativeRedirectScheme] we see an error during android build.
     */
    config.modResults.contents = config.modResults.contents.replace(
      'defaultConfig {',
      androidMultilineFileChanges.buildGradle
    );

    return config;
  });
};

const generateAndroidMainActivity = (cnfg) => {
  return withMainActivity(cnfg, (config) => {
    const mainActivity = config.modResults;
    mainActivity.contents = mainActivity.contents.replace(
      'import android.os.Bundle;',
      androidMultilineFileChanges.mainActivityImports
    );
    mainActivity.contents = mainActivity.contents.replace(
      'super.onCreate(null);',
      androidMultilineFileChanges.mainActivityCode
    );

    return config;
  });
};

/**
 * NOTE
 * This plugin follows the installation process for V1.2.0.
 */

const withMyAdyenConfig = (config) => {
  config = generateAndroidManifest(config);

  config = generateAndroidAppBuildGradle(config);

  config = generateAndroidMainActivity(config);

  config = generateIosAppDelegate(config);

  return config;
};

module.exports = withMyAdyenConfig;

@gunnartorfis
Copy link
Contributor

Any updates on this?

@gunnartorfis
Copy link
Contributor

Just tested this by patching the changes.
I got an error when building my iOS app about ADYRedirectComponent not being found.
What fixed it for me was to change the import statement to use underscore: #import <adyen_react_native/ADYRedirectComponent.h> - I think it is because I'm using use_frameworks!

@anthlasserre
Copy link
Contributor Author

Just tested this by patching the changes. I got an error when building my iOS app about ADYRedirectComponent not being found. What fixed it for me was to change the import statement to use underscore: #import <adyen_react_native/ADYRedirectComponent.h> - I think it is because I'm using use_frameworks!

Hey @gunnartorfis 👋🏼
Sorry about that issue. Indeed the import is different if you using use_frameworks.

Here is the plugin updated with that.

const {
  withAndroidManifest,
  withMainActivity,
  withAppDelegate,
} = require('@expo/config-plugins');

const withAdyenAndroid = config => {
  const configWithMainActivity = withMainActivity(config, async config => {
    const mainActivity = config.modResults;
    mainActivity.contents = mainActivity.contents.replace(
      'public class MainActivity extends ReactActivity {',
      'import com.adyenreactnativesdk.AdyenCheckout;\n\npublic class MainActivity extends ReactActivity {'
    );
    mainActivity.contents = mainActivity.contents.replace(
      'super.onCreate(null);\n  }',
      'super.onCreate(null);\n    AdyenCheckout.setLauncherActivity(this);\n  }'
    );
    return config;
  });

  const configWithManifest = withAndroidManifest(
    configWithMainActivity,
    async config => {
      const mainActivity = config.modResults;
      // Add com.adyenreactnativesdk.component.dropin.AdyenCheckoutService service
      // after com.facebook.react.HeadlessJsTaskService
      mainActivity.manifest.application = [
        // @ts-expect-error - manifest is not well typed
        {
          ...mainActivity.manifest.application?.[0],
          service: [
            {
              $: {
                'android:name':
                  'com.adyenreactnativesdk.component.dropin.AdyenCheckoutService',
                'android:exported': 'false',
              },
            },
          ],
        },
      ];
      return config;
    }
  );

  return configWithManifest;
};

const withAdyenIos = (config, iosFramework) => {
  const importLine = iosFramework === 'static' ? "#import <adyen_react_native/ADYRedirectComponent.h>" : "#import <adyen-react-native/ADYRedirectComponent.h>"
  const appDelegate = withAppDelegate(config, async config => {
    const appDelegate = config.modResults;
    appDelegate.contents = appDelegate.contents.replace(
      '#import "AppDelegate.h"\n\n',
      `#import "AppDelegate.h"\n\n${importLine}\n`
    );
    appDelegate.contents = appDelegate.contents.replace(
      /\/\/ Linking API.*\n.*\n.*\n}/g,
      `// Linking API
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
  // Adyen SDK
  return [ADYRedirectComponent applicationDidOpenURL:url];
}`
    );
    return config;
  });
  return appDelegate;
};

module.exports = function (config, iosFramework = 'dynamic') {
  return withAdyenIos(withAdyenAndroid(config), iosFramework);
};

You just need to call the plugin in your app.json in that way after (with dynamic or static):

{
  "plugins": [
    ["./plugins/adyen-react-native", "dynamic"],
  ]
}

I'll need to update the PR here. But we're waiting for help from the Ably team...

@gunnartorfis
Copy link
Contributor

@anthlasserre Nice, that was fast! 👏

We're just in the initial stages of integrating Adyen in our Expo app - would be lovely to get this merged so we don't have to use a patch on production.

@anthlasserre
Copy link
Contributor Author

@anthlasserre Nice, that was fast! 👏

We're just in the initial stages of integrating Adyen in our Expo app - would be lovely to get this merged so we don't have to use a patch on production.

No worries mate. Adyen is a good choice and we'll be all happy to help their team to bring the Expo support.

You don't necessarily need to create a patch I'd say. Just create a plugins folder at the root of your Expo app and add this plugin reference to your app.json or app.config.js as I have explained above. And you won't need any patch.

{
  "plugins": [
    ["./plugins/adyen-react-native", "dynamic"],
  ]
}

@anthlasserre
Copy link
Contributor Author

@descorp PR updated 🔄

@camil-adyen
Copy link

Hi all,

Thanks a lot for the PR and the feedback!

We are keeping track of your comments, but right now our full focus is on the v2 release. After the beta release, we will dive into this PR. Our goal is to release Expo support as a new feature for Adyen React Native v2 by the end of Q1 '24.

Stay tuned!

@gunnartorfis
Copy link
Contributor

Hi all,

Thanks a lot for the PR and the feedback!

We are keeping track of your comments, but right now our full focus is on the v2 release. After the beta release, we will dive into this PR. Our goal is to release Expo support as a new feature for Adyen React Native v2 by the end of Q1 '24.

Stay tuned!

@camil-adyen Thanks for the update.
One unrelated question, is there a draft of changes in v2? What are the biggest bites?

@descorp
Copy link
Contributor

descorp commented Jan 15, 2024

@gunnartorfis

is there a draft of changes in v2? What are the biggest bites?

Not yet, since we are still finalising API.
However, so far we can see that there are no breaking changes for "Advanced flow" (the default flow on v1).
We were even able to slightly decrease amount of mandatory code.

Will keep you posted.

@gunnartorfis
Copy link
Contributor

@anthlasserre the plugin wouldn't work with Expo SDK 50 (because of RN 0.73's Kotlin update).
Here's the small tweak that would make it compatible with that. I'm not sure how we could detect the Expo SDK version, I've never created a plugin before.

const configWithMainActivity = withMainActivity(config, async newConfig => {
  const mainActivity = newConfig.modResults;
  mainActivity.contents = mainActivity.contents.replace(
    'class MainActivity : ReactActivity() {',
    'import com.adyenreactnativesdk.AdyenCheckout;\n\nclass MainActivity : ReactActivity() {',
  );
  mainActivity.contents = mainActivity.contents.replace(
    'super.onCreate(null)',
    'super.onCreate(null);\n    AdyenCheckout.setLauncherActivity(this);',
  );
  return newConfig;
});

@descorp
Copy link
Contributor

descorp commented Apr 5, 2024

Hey @anthlasserre

We have completed initial work on supporting Expo for Beta v2 #397

Thank you for your PR! It was very helpful and inspiring for us 💚
I am going to close it.

@descorp descorp closed this Apr 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants