diff --git a/Kits/RoktStripePayment/package.json b/Kits/RoktStripePayment/package.json new file mode 100644 index 0000000..e8a7e9e --- /dev/null +++ b/Kits/RoktStripePayment/package.json @@ -0,0 +1,23 @@ +{ + "name": "@mparticle/cordova-rokt-stripe-payment-kit", + "version": "3.0.0", + "description": "Adds Rokt Stripe payment extension (Apple Pay) support to your Cordova project", + "homepage": "https://www.mparticle.com", + "repository": { + "type": "git", + "url": "https://github.com/mParticle/cordova-plugin-mparticle", + "directory": "Kits/RoktStripePayment" + }, + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "provenance": true, + "registry": "https://registry.npmjs.org/" + }, + "cordova": { + "id": "@mparticle/cordova-rokt-stripe-payment-kit", + "platforms": [ + "ios" + ] + } +} diff --git a/Kits/RoktStripePayment/plugin.xml b/Kits/RoktStripePayment/plugin.xml new file mode 100644 index 0000000..206963c --- /dev/null +++ b/Kits/RoktStripePayment/plugin.xml @@ -0,0 +1,17 @@ + + + MParticle Rokt Stripe Payment Kit + Adds Rokt Stripe payment extension (Apple Pay) support to your Cordova project + + + + + + + + + + + diff --git a/README.md b/README.md index 6dd57a0..88a784f 100755 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ cordova plugin add @mparticle/cordova-sdk **Install the SDK** using CocoaPods: ```bash -$ # Update your Podfile to depend on 'mParticle-Apple-SDK' version 8.5.2 or later +$ # Update your Podfile to depend on 'mParticle-Apple-SDK' version 9.0 or later $ pod install ``` @@ -264,6 +264,89 @@ identity.modify(request, function (userId) => { }); ``` +## Rokt + +### Installation + +Add the Rokt kit to your `config.xml`: + +```xml + +``` + +### Select Placements + +Display a Rokt overlay placement on your confirmation or thank-you page: + +```js +var attributes = { + 'email': 'j.smith@example.com', + 'firstname': 'Jenny', + 'lastname': 'Smith', + 'confirmationref': '54321', + 'country': 'US' +}; + +mparticle.Rokt.selectPlacements('YourPlacementIdentifier', attributes); +``` + +You can optionally pass a configuration object: + +```js +var config = { + colorMode: mparticle.Rokt.ColorMode.SYSTEM, // LIGHT, DARK, or SYSTEM + cacheConfig: { + cacheDurationInSeconds: 5400, + cacheAttributes: {} + } +}; + +mparticle.Rokt.selectPlacements('YourPlacementIdentifier', attributes, config); +``` + +### Shoppable Ads + +Shoppable Ads enable post-purchase upsell offers with instant checkout via Apple Pay or Stripe. Currently supported on **iOS only**. + +#### 1. Add the Stripe payment kit + +```xml + +``` + +#### 2. Register the payment extension + +The `RoktStripePaymentExtension` must be registered in your native iOS code after SDK initialization and before calling `selectShoppableAds`. + +Since `RoktStripePaymentExtension` is Swift-only, ObjC apps need a bridging class. See `example/platform_overrides/ios/RoktPaymentSetup.swift` for the wrapper, then call from your AppDelegate: + +```objc +#import "YourApp-Swift.h" + +[RoktPaymentSetup registerPaymentExtensionWithMerchantId:@"merchant.com.yourapp.rokt"]; +``` + +#### 3. Display Shoppable Ads + +```js +var attributes = { + 'email': 'j.smith@example.com', + 'firstname': 'Jenny', + 'lastname': 'Smith', + 'confirmationref': 'ORD-8829-XK2', + 'amount': '52.25', + 'currency': 'USD', + 'paymenttype': 'visa', + 'shippingaddress1': '123 Main St', + 'shippingcity': 'Brooklyn', + 'shippingstate': 'NY', + 'shippingzipcode': '11201', + 'shippingcountry': 'US' +}; + +mparticle.Rokt.selectShoppableAds('YourPlacementIdentifier', attributes); +``` + # License Apache 2.0 diff --git a/example/config.xml b/example/config.xml index f226e75..3de2177 100644 --- a/example/config.xml +++ b/example/config.xml @@ -30,4 +30,5 @@ + diff --git a/example/hooks/after_platform_add/010_copy_platform_specific_files.js b/example/hooks/after_platform_add/010_copy_platform_specific_files.js index e8b9e0a..41a9818 100755 --- a/example/hooks/after_platform_add/010_copy_platform_specific_files.js +++ b/example/hooks/after_platform_add/010_copy_platform_specific_files.js @@ -7,7 +7,7 @@ var shell = require('child_process'); module.exports = function(context) { // Get platform from context var platform = context.opts.platforms[0]; - + function ensureDirectoryExistence(filePath) { var dirname = path.dirname(filePath); if (fs.existsSync(dirname)) { @@ -16,35 +16,64 @@ module.exports = function(context) { fs.mkdirSync(dirname, { recursive: true }); } - if (platform === 'android') { - // Copy MainActivity.java - var srcFile = path.join(context.opts.projectRoot, 'platform_overrides', 'android', 'MainActivity.java'); - var destFile = path.join(context.opts.projectRoot, 'platforms', 'android', 'app', 'src', 'main', 'java', 'com', 'mparticle', 'example', 'MainActivity.java'); - - if (fs.existsSync(srcFile)) { + function copyFile(srcRelative, destRelative, label) { + var src = path.join(context.opts.projectRoot, srcRelative); + var dest = path.join(context.opts.projectRoot, destRelative); + if (fs.existsSync(src)) { try { - ensureDirectoryExistence(destFile); - fs.copyFileSync(srcFile, destFile); - console.log('Copied custom MainActivity.java'); + ensureDirectoryExistence(dest); + fs.copyFileSync(src, dest); + console.log('Copied ' + label); } catch (err) { - console.error('Error copying MainActivity.java:', err); + console.error('Error copying ' + label + ':', err); process.exitCode = 1; } } + } + + if (platform === 'android') { + copyFile( + path.join('platform_overrides', 'android', 'MainActivity.java'), + path.join('platforms', 'android', 'app', 'src', 'main', 'java', 'com', 'mparticle', 'example', 'MainActivity.java'), + 'custom MainActivity.java' + ); } else if (platform === 'ios') { - // Copy AppDelegate.m - var srcFile = path.join(context.opts.projectRoot, 'platform_overrides', 'ios', 'AppDelegate.m'); - var destFile = path.join(context.opts.projectRoot, 'platforms', 'ios', 'MParticleExample', 'AppDelegate.m'); - - if (fs.existsSync(srcFile)) { + copyFile( + path.join('platform_overrides', 'ios', 'AppDelegate.m'), + path.join('platforms', 'ios', 'MParticleExample', 'AppDelegate.m'), + 'custom AppDelegate.m' + ); + + copyFile( + path.join('platform_overrides', 'ios', 'RoktPaymentSetup.swift'), + path.join('platforms', 'ios', 'MParticleExample', 'RoktPaymentSetup.swift'), + 'RoktPaymentSetup.swift' + ); + + // Create bridging header for Swift/ObjC interop + var bridgingHeaderPath = path.join(context.opts.projectRoot, 'platforms', 'ios', 'MParticleExample', 'MParticleExample-Bridging-Header.h'); + if (!fs.existsSync(bridgingHeaderPath)) { try { - ensureDirectoryExistence(destFile); - fs.copyFileSync(srcFile, destFile); - console.log('Copied custom AppDelegate.m'); + fs.writeFileSync(bridgingHeaderPath, '// Bridging header for Swift/ObjC interop\n'); + console.log('Created bridging header'); } catch (err) { - console.error('Error copying AppDelegate.m:', err); + console.error('Error creating bridging header:', err); process.exitCode = 1; } } + + // NOTE: RoktStripePaymentExtension pod is added automatically by the + // @mparticle/cordova-rokt-stripe-payment-kit declared in config.xml. + // If using a kit, no manual Podfile manipulation is needed here. + + // Add Swift file to Xcode project and configure Swift settings + var iosPath = path.join(context.opts.projectRoot, 'platforms', 'ios'); + var configureXcodeScript = path.join(context.opts.projectRoot, 'hooks', 'after_platform_add', 'configure_xcode_swift.rb'); + try { + shell.execSync('cd ' + iosPath + ' && ruby ' + configureXcodeScript, { stdio: 'inherit' }); + } catch (err) { + console.error('Error configuring Xcode project for Swift:', err); + process.exitCode = 1; + } } }; diff --git a/example/hooks/after_platform_add/configure_xcode_swift.rb b/example/hooks/after_platform_add/configure_xcode_swift.rb new file mode 100644 index 0000000..0002cda --- /dev/null +++ b/example/hooks/after_platform_add/configure_xcode_swift.rb @@ -0,0 +1,16 @@ +require 'xcodeproj' + +proj = Xcodeproj::Project.open('MParticleExample.xcodeproj') +target = proj.targets.find { |t| t.name == 'MParticleExample' } +group = proj.main_group.find_subpath('MParticleExample', false) + +swift_ref = group.new_file('RoktPaymentSetup.swift') +target.source_build_phase.add_file_reference(swift_ref) + +target.build_configurations.each do |config| + config.build_settings['SWIFT_VERSION'] = '5.0' + config.build_settings['SWIFT_OBJC_BRIDGING_HEADER'] = 'MParticleExample/MParticleExample-Bridging-Header.h' +end + +proj.save +puts 'Configured Xcode project for Swift/Shoppable Ads' diff --git a/example/platform_overrides/ios/AppDelegate.m b/example/platform_overrides/ios/AppDelegate.m index 798becb..23b1dd2 100644 --- a/example/platform_overrides/ios/AppDelegate.m +++ b/example/platform_overrides/ios/AppDelegate.m @@ -25,6 +25,7 @@ Licensed to the Apache Software Foundation (ASF) under one #import "AppDelegate.h" #import "MainViewController.h" #import "mParticle.h" +#import "MParticleExample-Swift.h" @implementation AppDelegate @@ -41,8 +42,11 @@ - (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(N }; [[MParticle sharedInstance] startWithOptions:mParticleOptions]; - + [MParticle sharedInstance].logLevel = MPILogLevelVerbose; + + // Register Rokt Stripe payment extension for Shoppable Ads + [RoktPaymentSetup registerPaymentExtensionWithMerchantId:@"merchant.com.mparticle.example"]; self.viewController = [[MainViewController alloc] init]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; diff --git a/example/platform_overrides/ios/RoktPaymentSetup.swift b/example/platform_overrides/ios/RoktPaymentSetup.swift new file mode 100644 index 0000000..80f7174 --- /dev/null +++ b/example/platform_overrides/ios/RoktPaymentSetup.swift @@ -0,0 +1,11 @@ +import Foundation +import RoktStripePaymentExtension +import mParticle_Apple_SDK_ObjC + +@objc public class RoktPaymentSetup: NSObject { + @objc public static func registerPaymentExtension(merchantId: String) { + if let stripeExt = RoktStripePaymentExtension(applePayMerchantId: merchantId) { + MParticle.sharedInstance().rokt.register(stripeExt) + } + } +} diff --git a/example/www/index.html b/example/www/index.html index 52d6a92..9c4cb26 100644 --- a/example/www/index.html +++ b/example/www/index.html @@ -44,6 +44,8 @@

mParticle


Rokt

+ + diff --git a/example/www/js/index.js b/example/www/js/index.js index 264060e..606b903 100644 --- a/example/www/js/index.js +++ b/example/www/js/index.js @@ -12,6 +12,8 @@ document.getElementById("setATTStatusBtn").addEventListener('click', setATTStatu document.getElementById("loginBtn").addEventListener('click', login, false); document.getElementById("logoutBtn").addEventListener('click', logout, false); document.getElementById("trackConversionBtn").addEventListener('click', trackConversion, false); +document.getElementById("selectShoppableAdsBtn").addEventListener('click', selectShoppableAds, false); +document.getElementById("purchaseFinalizedBtn").addEventListener('click', purchaseFinalized, false); var app = { // Application Constructor @@ -244,4 +246,37 @@ function trackConversion() { ); } +function selectShoppableAds() { + console.log('MParticleCordova Plugin Example: Selecting Shoppable Ads'); + + var attributes = { + 'email': 'j.smith@example.com', + 'firstname': 'Jenny', + 'lastname': 'Smith', + 'country': 'US', + 'shippingstate': 'NY', + 'shippingzipcode': '10001', + 'confirmationref': '54321' + }; + + var config = { + colorMode: mparticle.Rokt.ColorMode.SYSTEM, + cacheConfig: { + cacheDurationInSeconds: 5400, + cacheAttributes: {} + } + }; + + mparticle.Rokt.selectShoppableAds( + 'StgRoktShoppableAds', + attributes, + config + ); +} + +function purchaseFinalized() { + console.log('MParticleCordova Plugin Example: Purchase Finalized'); + mparticle.Rokt.purchaseFinalized('StgRoktShoppableAds', 'catalog-item-123', true); +} + app.initialize(); diff --git a/plugin/src/android/src/main/java/com/mparticle/cordova/MParticleCordovaPlugin.java b/plugin/src/android/src/main/java/com/mparticle/cordova/MParticleCordovaPlugin.java index 189a68a..e5573cd 100644 --- a/plugin/src/android/src/main/java/com/mparticle/cordova/MParticleCordovaPlugin.java +++ b/plugin/src/android/src/main/java/com/mparticle/cordova/MParticleCordovaPlugin.java @@ -96,6 +96,12 @@ public boolean execute(String action, final JSONArray args, final CallbackContex } else if (action.equals("selectPlacements")) { selectPlacements(args); return true; + } else if (action.equals("selectShoppableAds")) { + selectShoppableAds(args); + return true; + } else if (action.equals("purchaseFinalized")) { + purchaseFinalized(args); + return true; } else { return false; } @@ -442,6 +448,18 @@ public void selectPlacements(final JSONArray args) throws JSONException { ); } + public void selectShoppableAds(final JSONArray args) throws JSONException { + Logger.warning("selectShoppableAds is not yet supported on Android"); + } + + public void purchaseFinalized(final JSONArray args) throws JSONException { + final String placementId = args.getString(0); + final String catalogItemId = args.getString(1); + final boolean success = args.getBoolean(2); + + MParticle.getInstance().Rokt().purchaseFinalized(placementId, catalogItemId, success); + } + private static IdentityApiRequest ConvertIdentityAPIRequest(JSONObject map) throws JSONException { IdentityApiRequest.Builder identityRequest = IdentityApiRequest.withEmptyUser(); diff --git a/plugin/src/ios/CDVMParticle.m b/plugin/src/ios/CDVMParticle.m index d08e350..688bef1 100644 --- a/plugin/src/ios/CDVMParticle.m +++ b/plugin/src/ios/CDVMParticle.m @@ -330,7 +330,7 @@ - (void)getUserIdentities:(CDVInvokedUrlCommand*)command { } - (RoktConfig *)buildRoktConfigFromDict:(NSDictionary *)configDict { - if (configDict == nil || configDict.count == 0) { + if (configDict == nil || [configDict isKindOfClass:[NSNull class]] || configDict.count == 0) { return nil; } @@ -384,6 +384,39 @@ - (void)selectPlacements:(CDVInvokedUrlCommand*)command { }]; } +- (void)selectShoppableAds:(CDVInvokedUrlCommand*)command { + [self.commandDelegate runInBackground:^{ + NSString *identifier = [command.arguments objectAtIndex:0]; + NSDictionary *attributes = [command.arguments objectAtIndex:1]; + NSDictionary *configDict = [command.arguments objectAtIndex:2]; + + RoktConfig *config = [self buildRoktConfigFromDict:configDict]; + + [[MParticle sharedInstance].rokt selectShoppableAds:identifier + attributes:attributes + config:config + onEvent:nil]; + + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; + }]; +} + +- (void)purchaseFinalized:(CDVInvokedUrlCommand*)command { + [self.commandDelegate runInBackground:^{ + NSString *identifier = [command.arguments objectAtIndex:0]; + NSString *catalogItemId = [command.arguments objectAtIndex:1]; + BOOL success = [[command.arguments objectAtIndex:2] boolValue]; + + [[MParticle sharedInstance].rokt purchaseFinalized:identifier + catalogItemId:catalogItemId + success:success]; + + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; + }]; +} + typedef NS_ENUM(NSUInteger, MPCDVCommerceEventAction) { MPCDVCommerceEventActionAddToCart = 1, MPCDVCommerceEventActionRemoveFromCart, diff --git a/plugin/www/mparticle.js b/plugin/www/mparticle.js index 69797b6..e195dda 100644 --- a/plugin/www/mparticle.js +++ b/plugin/www/mparticle.js @@ -482,6 +482,35 @@ var mparticle = { } exec('selectPlacements', [identifier, attributes || {}, finalConfig]) + }, + + selectShoppableAds: function (identifier, attributes, config) { + var finalConfig = null + if (config) { + var requestedColorMode = config.colorMode || mparticle.Rokt.ColorMode.SYSTEM + requestedColorMode = mparticle.Rokt.validateColorMode(requestedColorMode) + + finalConfig = { + colorMode: { + value: requestedColorMode + } + } + + if (config.cacheConfig) { + finalConfig.cacheConfig = { + cacheDurationInSeconds: config.cacheConfig.cacheDurationInSeconds != null + ? config.cacheConfig.cacheDurationInSeconds + : 5400, + cacheAttributes: config.cacheConfig.cacheAttributes || {} + } + } + } + + exec('selectShoppableAds', [identifier, attributes || {}, finalConfig]) + }, + + purchaseFinalized: function (placementId, catalogItemId, success) { + exec('purchaseFinalized', [placementId, catalogItemId, !!success]) } } }