square / in-app-payments-react-native-plugin

Apache License 2.0
118 stars 50 forks source link

[expo] Plugins for adding build script also merchantID and enable google pay #236

Closed kuldip-simform closed 5 months ago

kuldip-simform commented 5 months ago

Describe the issue

I have updated to new version 1.7.4 from 1.7.2. After that When I open app, app immediately crashes on both real device and simulator

Here is the log:

CrashReporter Key:   13561001-68E7-A1F7-C6A3-657E6BF1CD4F
Hardware Model:      MacBookAir10,1
Process:             LouisvilleSluggerStaging [14616]
Path:                /Users/USER/Library/Developer/CoreSimulator/Devices/53AE8839-E9C6-4D2F-B9BF-142A34E152C2/data/Containers/Bundle/Application/11DFA023-7A1F-4F6D-9375-8B61277B8FDE/LouisvilleSluggerStaging.app/LouisvilleSluggerStaging
Identifier:          com.twentyfourcorp.louisvillesluggerstaging
Version:             1.0.0 (1)
Code Type:           ARM-64 (Native)
Role:                Foreground
Parent Process:      launchd_sim [12500]
Coalition:           com.apple.CoreSimulator.SimDevice.53AE8839-E9C6-4D2F-B9BF-142A34E152C2 [28900]
Responsible Process: SimulatorTrampoline [11669]

Date/Time:           2024-04-22 16:08:23.0041 +0530
Launch Time:         2024-04-22 16:08:22.5014 +0530
OS Version:          macOS 13.5.1 (22G90)
Release Type:        User
Report Version:      104

Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Termination Reason: DYLD 1 Library missing
Library not loaded: @rpath/CorePaymentCard.framework/CorePaymentCard
Referenced from: <1E7AA7BA-B4D0-355A-B5B0-53A2B470F9C6> /Users/kuldipsoliya/Library/Developer/CoreSimulator/Devices/53AE8839-E9C6-4D2F-B9BF-142A34E152C2/data/Containers/Bundle/Application/11DFA023-7A1F-4F6D-9375-8B61277B8FDE/LouisvilleSluggerStaging.app/Frameworks/SquareBuyerVerificationSDK.framework/SquareBuyerVerificationSDK
Reason: tried: '/Library/Developer/CoreSimulator/Volumes/iOS_21C62/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.2.simruntime/Contents/Resources/RuntimeRoot/usr/lib/swift/CorePaymentCard.framework/CorePaymentCard' (no such file), '/usr/lib/swift/CorePaymentCard.framework/CorePaymentCard' (no such file, no dyld cache), '/Library/Developer/CoreSimulator/Volumes/iOS_21C62/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.2.simruntime/Contents/Resources/RuntimeRoot/usr/lib/swift/CorePaymentCard.framework/CorePaymentCard' (no such file), '/usr/lib/swift/CorePaymentCard.framework/CorePaymentCard' (no s
(terminated at launch; ignore backtrace)

To Reproduce

Steps to reproduce the issue.

For example -

  1. Add version 1.7.5 to expo 50 project
  2. open app on iOS device

Expected behavior

app should run without any issue.

Environment (please complete the following information):

System:
  OS: macOS 13.5.1
  CPU: (8) arm64 Apple M1
  Memory: 170.86 MB / 16.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 21.6.2
    path: /opt/homebrew/bin/node
  Yarn:
    version: 1.22.18
    path: /opt/homebrew/bin/yarn
  npm:
    version: 10.2.4
    path: /opt/homebrew/bin/npm
  Watchman:
    version: 2024.01.22.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.15.2
    path: /opt/homebrew/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 23.2
      - iOS 17.2
      - macOS 14.2
      - tvOS 17.2
      - visionOS 1.0
      - watchOS 10.2
  Android SDK: Not Found
IDEs:
  Android Studio: 2022.3 AI-223.8836.35.2231.10671973
  Xcode:
    version: 15.2/15C500b
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 11.0.15
    path: /usr/bin/javac
  Ruby:
    version: 2.6.10
    path: /usr/bin/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react:
    installed: 18.2.0
    wanted: 18.2.0
  react-native:
    installed: 0.73.6
    wanted: 0.73.6
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: Not found
  newArchEnabled: Not found
iOS:
  hermesEnabled: Not found
  newArchEnabled: Not found

Screenshots

https://github.com/square/in-app-payments-react-native-plugin/assets/104821076/bd969b21-f3c9-4216-a644-42ae2942e4d9

kuldip-simform commented 5 months ago

The issue was from my side I forgot to add build phase which is mentioned here in iOS SDK documentation.

But as I am using Expo and there is no plugins provided by Square, I have to create my own to add merchantIds in xcode and enable google pay in android side.also I had to create plugin to add build phase in xcode project build phase. Now issue arise due to order of build phase.

All the plugins run before pod install in expo and after pod install [CP] Embed Pods Frameworks script gets added. whereas square script needs to last.

Screenshot 2024-04-23 at 4 08 09 PM

So I had to right ruby script to re-order build script to be last and run this script in eas-build-post-install script. so this will be run after pod-install is done.

Re-ordering build phase script

#!/usr/bin/env ruby
require 'xcodeproj'
require 'set'

project_file, target_name = ARGV

puts "Sorting sources in #{project_file} for target #{target_name}"

project = Xcodeproj::Project.open(project_file)
target = project.targets.select { |t| t.name == target_name }.first
square_framework_run_script_index = target.build_phases.index { |b|
    name = b.name if b.respond_to? :name
    name == "Square Framework Run Script - InAppPaymentsSDK" #name of build phase you have given in plugin below
}
puts "Square Framework Run Script index: #{square_framework_run_script_index} Total build phases: #{target.build_phases.count - 1}"
if square_framework_run_script_index.nil? == false
    puts "Moving Square Framework Run Script from #{square_framework_run_script_index} to the #{target.build_phases.count - 1} index"
    target.build_phases.move_from(square_framework_run_script_index, target.build_phases.count - 1) # move to the last indexs
end
project.save

Plugin for adding script in Xcode

const { withXcodeProject } = require('@expo/config-plugins');

const addBuildPhaseForSquareIOS = (config) => {
    return withXcodeProject(config, async (conf) => {
        const project = conf.modResults;

        project.addBuildPhase(
            [],
            'PBXShellScriptBuildPhase',
            'Square Framework Run Script - InAppPaymentsSDK', // build phase name
            project.getFirstTarget().uuid,
            {
                shellPath: '/bin/sh',
                shellScript:
                    'FRAMEWORKS="${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}" && "${FRAMEWORKS}/SquareInAppPaymentsSDK.framework/setup"',
            },
        );

        return conf;
    });
};

module.exports = addBuildPhaseForSquareIOS;

Add this plugin in app.json or app.config.ts. like this

{
  "expo": {
    ...,
    plugins: [
      ...,
      [
        "./appleGooglePayConfigPlugin.js",
        {
          "merchantIdentifier": "your merchant id",
          "enableGooglePay": true
        }
      ],
      [
        "./squareIOSBuildPhasePlugin.js"
      ],

    ],

  }
}

script file for adding that to package.json like: "eas-build-post-install": "./eas-hooks/eas-build-post-install.sh"

#!/usr/bin/env bash

# This is a file called "pre-install" in the root of the project
if [[ "$EAS_BUILD_PLATFORM" == "ios" ]]; then
    ./reorderSquareIAPBuildPhase.rb ios/your_project_name.xcodeproj your_target_name
fi

I have also written another plugin for adding merchantsId and google pay to true.

const {
    AndroidConfig,
    ConfigPlugin,
    IOSConfig,
    withAndroidManifest,
    withEntitlementsPlist,
} = require('@expo/config-plugins');

const { addMetaDataItemToMainApplication, getMainApplicationOrThrow, removeMetaDataItemFromMainApplication } =
    AndroidConfig.Manifest;

// type SquarePluginProps = {
//     /**
//      * The iOS merchant ID used for enabling Apple Pay.
//      * Without this, the error "Missing merchant identifier" will be thrown on iOS.
//      */
//     merchantIdentifier: string | string[];
//     enableGooglePay: boolean;
// };

const withSquareIos = (expoConfig, { merchantIdentifier }) => {
    return withEntitlementsPlist(expoConfig, (config) => {
        config.modResults = setApplePayEntitlement(merchantIdentifier, config.modResults);
        return config;
    });
};

const withSquare = (config, props) => {
    config = withSquareIos(config, props);
    config = withNoopSwiftFile(config);
    config = withSquareAndroid(config, props);
    return config;
};

/**
 * Adds the following to the entitlements:
 *
 * <key>com.apple.developer.in-app-payments</key>
 * <array>
 *   <string>[MERCHANT_IDENTIFIER]</string>
 * </array>
 */
function setApplePayEntitlement(merchantIdentifiers, entitlements) {
    const key = 'com.apple.developer.in-app-payments';

    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    const merchants = entitlements[key] ?? [];

    if (!Array.isArray(merchantIdentifiers)) {
        merchantIdentifiers = [merchantIdentifiers];
    }

    for (const id of merchantIdentifiers) {
        if (id && !merchants.includes(id)) {
            merchants.push(id);
        }
    }

    if (merchants.length) {
        entitlements[key] = merchants;
    }
    return entitlements;
}

/**
 * Add a blank Swift file to the Xcode project for Swift compatibility.
 */
const withNoopSwiftFile = (config) => {
    return IOSConfig.XcodeProjectFile.withBuildSourceFile(config, {
        filePath: 'noop-file.swift',
        contents: [
            '//',
            '// @generated',
            '// A blank Swift file must be created for native modules with Swift files to work correctly.',
            '//',
            '',
        ].join('\n'),
    });
};

const withSquareAndroid = (expoConfig, { enableGooglePay = false }) => {
    return withAndroidManifest(expoConfig, (config) => {
        config.modResults = setGooglePayMetaData(enableGooglePay, config.modResults);

        return config;
    });
};

/**
 * Adds the following to AndroidManifest.xml:
 *
 * <application>
 *   ...
 *   <meta-data
 *     android:name="com.google.android.gms.wallet.api.enabled"
 *     android:value="true|false" />
 * </application>
 */
function setGooglePayMetaData(enabled, modResults) {
    const GOOGLE_PAY_META_NAME = 'com.google.android.gms.wallet.api.enabled';
    const mainApplication = getMainApplicationOrThrow(modResults);
    if (enabled) {
        addMetaDataItemToMainApplication(mainApplication, GOOGLE_PAY_META_NAME, 'true');
    } else {
        removeMetaDataItemFromMainApplication(mainApplication, GOOGLE_PAY_META_NAME);
    }

    return modResults;
}

module.exports = withSquare;
timmyjose commented 2 months ago

@kuldip-simform Your comment has been incredibly helpful. Thank you for sharing your work! 🙏

kuldip-simform commented 2 months ago

Thank you @timmyjose . Let's hope we get first party support from square for this so not everyone has to write this themselves.

timmyjose commented 2 months ago

@kuldip-simform Sorry to bother you, but this is something that I'm not able to figure out (relatively new to React Native and frontend). Have you by chance encountered this error while invoking something like:

   await SQIPApplePay.requestApplePayNonce(
          {
            price: '1.00',
            summaryLabel: 'Test Item',
            countryCode: 'US',
            currencyCode: 'USD',
            paymentType: SQIPApplePay.PaymentTypeFinal,
          },
          onApplePayRequestNonceSuccess,
          onApplePayRequestNonceFailure,
          onApplePayComplete,
        )

Error:

[UIKitCore] Attempt to present <PKPaymentAuthorizationViewController: 0x1308094b0> on <UIViewController: 0x10831cb60> (from <UIViewController: 0x10831cb60>) which is already presenting <RNSScreen: 0x17f2fcc00>.

I've been trying to look this up, but even without any modal (from what I understand about the error message), I still see this for iOS, and it's become a blocker. 🙁

(If you haven't seen this, no worries - your plugin code has already been super helpful for us, and saved us a lot of time and effort! 🙏)

kuldip-simform commented 2 months ago

@timmyjose Fortunately I have not encounter this error (😀) so I can't help you in this problem.
Yes, you are right in understanding that this error would be due to you are presenting another modal screen on top of one.

timmyjose commented 2 months ago

@kuldip-simform Thank you for affirming that that is the issue - yes, I suppose it is very much because of the way the UI in my app is structured. Will look into simplifying it. Glad to hear that you didn't run into this issue, and thank you for all the help all the same! 🙂

SeanBarker182 commented 1 month ago

The issue was from my side I forgot to add build phase which is mentioned here in iOS SDK documentation.

But as I am using Expo and there is no plugins provided by Square, I have to create my own to add merchantIds in xcode and enable google pay in android side.also I had to create plugin to add build phase in xcode project build phase. Now issue arise due to order of build phase.

All the plugins run before pod install in expo and after pod install [CP] Embed Pods Frameworks script gets added. whereas square script needs to last.

Screenshot 2024-04-23 at 4 08 09 PM

So I had to right ruby script to re-order build script to be last and run this script in eas-build-post-install script. so this will be run after pod-install is done.

Re-ordering build phase script

#!/usr/bin/env ruby
require 'xcodeproj'
require 'set'

project_file, target_name = ARGV

puts "Sorting sources in #{project_file} for target #{target_name}"

project = Xcodeproj::Project.open(project_file)
target = project.targets.select { |t| t.name == target_name }.first
square_framework_run_script_index = target.build_phases.index { |b|
    name = b.name if b.respond_to? :name
    name == "Square Framework Run Script - InAppPaymentsSDK" #name of build phase you have given in plugin below
}
puts "Square Framework Run Script index: #{square_framework_run_script_index} Total build phases: #{target.build_phases.count - 1}"
if square_framework_run_script_index.nil? == false
    puts "Moving Square Framework Run Script from #{square_framework_run_script_index} to the #{target.build_phases.count - 1} index"
    target.build_phases.move_from(square_framework_run_script_index, target.build_phases.count - 1) # move to the last indexs
end
project.save

Plugin for adding script in Xcode

const { withXcodeProject } = require('@expo/config-plugins');

const addBuildPhaseForSquareIOS = (config) => {
    return withXcodeProject(config, async (conf) => {
        const project = conf.modResults;

        project.addBuildPhase(
            [],
            'PBXShellScriptBuildPhase',
            'Square Framework Run Script - InAppPaymentsSDK', // build phase name
            project.getFirstTarget().uuid,
            {
                shellPath: '/bin/sh',
                shellScript:
                    'FRAMEWORKS="${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}" && "${FRAMEWORKS}/SquareInAppPaymentsSDK.framework/setup"',
            },
        );

        return conf;
    });
};

module.exports = addBuildPhaseForSquareIOS;

Add this plugin in app.json or app.config.ts. like this

{
  "expo": {
    ...,
    plugins: [
      ...,
      [
        "./appleGooglePayConfigPlugin.js",
        {
          "merchantIdentifier": "your merchant id",
          "enableGooglePay": true
        }
      ],
      [
        "./squareIOSBuildPhasePlugin.js"
      ],

    ],

  }
}

script file for adding that to package.json like: "eas-build-post-install": "./eas-hooks/eas-build-post-install.sh"

#!/usr/bin/env bash

# This is a file called "pre-install" in the root of the project
if [[ "$EAS_BUILD_PLATFORM" == "ios" ]]; then
    ./reorderSquareIAPBuildPhase.rb ios/your_project_name.xcodeproj your_target_name
fi

I have also written another plugin for adding merchantsId and google pay to true.

const {
    AndroidConfig,
    ConfigPlugin,
    IOSConfig,
    withAndroidManifest,
    withEntitlementsPlist,
} = require('@expo/config-plugins');

const { addMetaDataItemToMainApplication, getMainApplicationOrThrow, removeMetaDataItemFromMainApplication } =
    AndroidConfig.Manifest;

// type SquarePluginProps = {
//     /**
//      * The iOS merchant ID used for enabling Apple Pay.
//      * Without this, the error "Missing merchant identifier" will be thrown on iOS.
//      */
//     merchantIdentifier: string | string[];
//     enableGooglePay: boolean;
// };

const withSquareIos = (expoConfig, { merchantIdentifier }) => {
    return withEntitlementsPlist(expoConfig, (config) => {
        config.modResults = setApplePayEntitlement(merchantIdentifier, config.modResults);
        return config;
    });
};

const withSquare = (config, props) => {
    config = withSquareIos(config, props);
    config = withNoopSwiftFile(config);
    config = withSquareAndroid(config, props);
    return config;
};

/**
 * Adds the following to the entitlements:
 *
 * <key>com.apple.developer.in-app-payments</key>
 * <array>
 *     <string>[MERCHANT_IDENTIFIER]</string>
 * </array>
 */
function setApplePayEntitlement(merchantIdentifiers, entitlements) {
    const key = 'com.apple.developer.in-app-payments';

    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    const merchants = entitlements[key] ?? [];

    if (!Array.isArray(merchantIdentifiers)) {
        merchantIdentifiers = [merchantIdentifiers];
    }

    for (const id of merchantIdentifiers) {
        if (id && !merchants.includes(id)) {
            merchants.push(id);
        }
    }

    if (merchants.length) {
        entitlements[key] = merchants;
    }
    return entitlements;
}

/**
 * Add a blank Swift file to the Xcode project for Swift compatibility.
 */
const withNoopSwiftFile = (config) => {
    return IOSConfig.XcodeProjectFile.withBuildSourceFile(config, {
        filePath: 'noop-file.swift',
        contents: [
            '//',
            '// @generated',
            '// A blank Swift file must be created for native modules with Swift files to work correctly.',
            '//',
            '',
        ].join('\n'),
    });
};

const withSquareAndroid = (expoConfig, { enableGooglePay = false }) => {
    return withAndroidManifest(expoConfig, (config) => {
        config.modResults = setGooglePayMetaData(enableGooglePay, config.modResults);

        return config;
    });
};

/**
 * Adds the following to AndroidManifest.xml:
 *
 * <application>
 *   ...
 *     <meta-data
 *     android:name="com.google.android.gms.wallet.api.enabled"
 *     android:value="true|false" />
 * </application>
 */
function setGooglePayMetaData(enabled, modResults) {
    const GOOGLE_PAY_META_NAME = 'com.google.android.gms.wallet.api.enabled';
    const mainApplication = getMainApplicationOrThrow(modResults);
    if (enabled) {
        addMetaDataItemToMainApplication(mainApplication, GOOGLE_PAY_META_NAME, 'true');
    } else {
        removeMetaDataItemFromMainApplication(mainApplication, GOOGLE_PAY_META_NAME);
    }

    return modResults;
}

module.exports = withSquare;

You can also move the ruby script to a config plugin:

const { withXcodeProject } = require("@expo/config-plugins");

const withReorderSquareBuildPhase = (config) => {
  return withXcodeProject(config, async (config) => {
    const xcodeProject = config.modResults;
    const target = xcodeProject.getFirstTarget().firstTarget;
    const targetName = target.name;
    console.log(
      `[withReorderSquareBuildPhase] Reordering build phases for target ${targetName}...`,
    );

    const squareFrameworkRunScriptIndex = target.buildPhases?.findIndex(
      (buildPhase) => {
        const buildPhaseName = buildPhase.name;
        return buildPhaseName === "Configure SQIP SDK for iOS";
      },
    );
    if (squareFrameworkRunScriptIndex !== -1) {
      console.log(
        `[withReorderSquareBuildPhase] Moving Square Framework Run Script from ${squareFrameworkRunScriptIndex} to the ${xcodeProject.pbxNativeTargetSection(targetName).buildPhases.length - 1} index`,
      );

      target.buildPhases.move(
        squareFrameworkRunScriptIndex,
        xcodeProject.pbxNativeTargetSection(targetName).buildPhases.length - 1,
      );
    }
    return config;
  });
};

module.exports = withReorderSquareBuildPhase;