From 59832865d94e6873df657aa516b83f2794495409 Mon Sep 17 00:00:00 2001 From: Daniel Hok Date: Tue, 6 Dec 2022 11:56:35 -0500 Subject: [PATCH] Release Flutter SDK version 3.0.0 --- .vscode/settings.json | 1 - CHANGELOG.md | 23 + example/ios/Podfile | 6 +- example/ios/Runner.xcodeproj/project.pbxproj | 43 +- example/ios/Runner/AppDelegate.swift | 94 ++- example/lib/main.dart | 154 ++-- example/pubspec.yaml | 2 +- ios/Classes/BrazePlugin.swift | 817 ++++++++++--------- ios/braze_plugin.podspec | 15 +- lib/braze_plugin.dart | 12 +- pubspec.yaml | 2 +- test/braze_plugin_test.dart | 7 + 12 files changed, 662 insertions(+), 514 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e2fe0da..4331077 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,5 @@ { "java.configuration.updateBuildConfiguration": "automatic", - "lldb.library": "/Applications/Xcode-14.0.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB", "githubPullRequests.ignoredPullRequestBranches": [ "develop" ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 4622e3f..a72f73d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +## 3.0.0 + +##### Breaking +- The native iOS bridge now uses the [new Braze Swift SDK](https://github.com/braze-inc/braze-swift-sdk), [version 5.6.4](https://github.com/braze-inc/braze-swift-sdk/blob/main/CHANGELOG.md#564). + - The minimum iOS deployment target is 10.0. +- During migration, update your project with the following changes: + - To initialize Braze, [follow these integration steps](https://braze-inc.github.io/braze-swift-sdk/tutorials/braze/a2-configure-braze) to create a `configuration` object. Then, add this code to complete the setup: + ``` + let braze = BrazePlugin.initBraze(configuration) + ``` + - To continue using `SDWebImage` as a dependency, add this line to your project's `/ios/Podfile`: + ``` + pod 'SDWebImage', :modular_headers => true + ``` + - Then, follow [these setup instructions](https://braze-inc.github.io/braze-swift-sdk/tutorials/braze/c3-gif-support). + - For guidance around other changes such as receiving in-app message and content card data, reference our sample [`AppDelegate.swift`](https://github.com/braze-inc/braze-flutter-sdk/blob/master/example/ios/Runner/AppDelegate.swift). + +##### Added +- Adds the `isControl` field to `BrazeContentCard`. + +##### Changed +- Updates the parameter syntax for `subscribeToInAppMessages()` and `subscribeToContentCards()`. + ## 2.6.1 ##### Added diff --git a/example/ios/Podfile b/example/ios/Podfile index 252d9ec..a8c979b 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '9.0' +platform :ios, '10.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -28,9 +28,11 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe flutter_ios_podfile_setup target 'Runner' do - use_frameworks! use_modular_headers! + # Add this line to use the default image loader + pod 'SDWebImage', :modular_headers => true + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 46d47d6..716f898 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -15,7 +15,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - EB2DDFF1B3E1144D693D4154 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCD5C61F3CC6ED57C5740EF8 /* Pods_Runner.framework */; }; + DA3B8E12704F8BFB709533C4 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BCDEDC224BBBA22EE83E5B26 /* libPods-Runner.a */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -49,7 +49,7 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AA8109965AD879D827565410 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - FCD5C61F3CC6ED57C5740EF8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BCDEDC224BBBA22EE83E5B26 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -57,7 +57,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - EB2DDFF1B3E1144D693D4154 /* Pods_Runner.framework in Frameworks */, + DA3B8E12704F8BFB709533C4 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -67,7 +67,7 @@ 0C8DAC57A8A97AA89DEBB028 /* Frameworks */ = { isa = PBXGroup; children = ( - FCD5C61F3CC6ED57C5740EF8 /* Pods_Runner.framework */, + BCDEDC224BBBA22EE83E5B26 /* libPods-Runner.a */, ); name = Frameworks; sourceTree = ""; @@ -150,7 +150,7 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - F3BA121F9263BCB4B89FAB00 /* [CP] Embed Pods Frameworks */, + C63C0F1FBCC5E8F10BB33202 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -225,7 +225,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -239,30 +239,26 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; - F3BA121F9263BCB4B89FAB00 /* [CP] Embed Pods Frameworks */ = { + C63C0F1FBCC5E8F10BB33202 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/Appboy-iOS-SDK/Appboy_iOS_SDK.framework", - "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", - "${BUILT_PRODUCTS_DIR}/braze_plugin/braze_plugin.framework", - "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/BrazeKit/BrazeKit.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/BrazeUI/BrazeUI.bundle", ); - name = "[CP] Embed Pods Frameworks"; + name = "[CP] Copy Pods Resources"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Appboy_iOS_SDK.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/braze_plugin.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/BrazeKit.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/BrazeUI.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; F49741450990356700E92D9D /* [CP] Check Pods Manifest.lock */ = { @@ -360,7 +356,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -381,7 +377,6 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -439,7 +434,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -486,7 +481,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; @@ -509,7 +504,6 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -539,7 +533,6 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 4df9b3e..20c2d2c 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,49 +1,85 @@ -import UIKit -import Appboy_iOS_SDK +import BrazeKit +import BrazeLocation +import BrazeUI import Flutter +import SDWebImage +import UIKit import braze_plugin -let apiKey = "9292484d-3b10-4e67-971d-ff0c0d518e21" +let brazeApiKey = "9292484d-3b10-4e67-971d-ff0c0d518e21" +let brazeEndpoint = "sondheim.appboy.com" @UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate, ABKInAppMessageControllerDelegate { +@objc class AppDelegate: FlutterAppDelegate, BrazeInAppMessageUIDelegate { + + static var braze: Braze? = nil + + // The subscription needs to be retained to be active + var contentCardsSubscription: Braze.Cancellable? - override func application(_ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) - Appboy.start(withApiKey: apiKey, - in:application, - withLaunchOptions:launchOptions, - withAppboyOptions: [ABKMinimumTriggerTimeIntervalKey : 1, - ABKEnableSDKAuthenticationKey : true]) - Appboy.sharedInstance()!.inAppMessageController.delegate = self + // - Setup Braze + let configuration = Braze.Configuration(apiKey: brazeApiKey, endpoint: brazeEndpoint) + configuration.sessionTimeout = 1 + configuration.triggerMinimumTimeInterval = 0 + configuration.location.automaticLocationCollection = true + configuration.location.brazeLocation = BrazeLocation() + configuration.logger.level = .debug + + let braze = BrazePlugin.initBraze(configuration) + + // - GIF support + GIFViewProvider.shared = .sdWebImage + + // - InAppMessage UI + let inAppMessageUI = BrazeInAppMessageUI() + inAppMessageUI.delegate = self + braze.inAppMessagePresenter = inAppMessageUI + + contentCardsSubscription = braze.contentCards.subscribeToUpdates { contentCards in + print("=> [Content Card Subscription] Received cards:", contentCards) - NotificationCenter.default.addObserver(self, selector: #selector(contentCardsUpdated), name:NSNotification.Name.ABKContentCardsProcessed, object: nil) + // Pass each content card model to the Dart layer. + BrazePlugin.processContentCards(contentCards) + } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } - func before(inAppMessageDisplayed inAppMessage: ABKInAppMessage) -> ABKInAppMessageDisplayChoice { - print("Received in-app message from Braze in beforeInAppMessageDisplayed delegate.") + // MARK: BrazeInAppMessageUIDelegate - // Pass in-app data to the Flutter layer. - BrazePlugin.processInAppMessage(inAppMessage) + func inAppMessage( + _ ui: BrazeInAppMessageUI, + willPresent message: Braze.InAppMessage, + view: InAppMessageView + ) { + print("=> [In-app Message] Received message from Braze:", message) - // Note: return ABKInAppMessageDisplayChoice.discardInAppMessage if you would like - // to prevent the Braze SDK from displaying the message natively. - return ABKInAppMessageDisplayChoice.displayInAppMessageNow + // Pass in-app message data to the Dart layer. + BrazePlugin.processInAppMessage(message) } - @objc private func contentCardsUpdated(_ notification: Notification) { - guard (notification.userInfo?[ABKContentCardsProcessedIsSuccessfulKey] as? Bool) == true, - let appboy = Appboy.sharedInstance() else { - return - } - - // Pass in-app data to the Flutter layer. - let contentCards = appboy.contentCardsController.contentCards.compactMap { $0 as? ABKContentCard } - BrazePlugin.processContentCards(contentCards) +} + +// MARK: GIF support + +extension GIFViewProvider { + + public static let sdWebImage = Self( + view: { SDAnimatedImageView(image: image(for: $0)) }, + updateView: { ($0 as? SDAnimatedImageView)?.image = image(for: $1) } + ) + + private static func image(for url: URL?) -> UIImage? { + guard let url = url else { return nil } + return url.pathExtension == "gif" + ? SDAnimatedImage(contentsOfFile: url.path) + : UIImage(contentsOfFile: url.path) } + } diff --git a/example/lib/main.dart b/example/lib/main.dart index 6b58e27..7faf1c4 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -238,47 +238,7 @@ class BrazeFunctionsState extends State { )); }, ), - TextButton( - child: const Text('SET LAST KNOWN LOCATION'), - onPressed: () { - print( - 'Requesting location initialization (no-op on iOS) and setting last known location'); - _braze.requestLocationInitialization(); - _braze.setLastKnownLocation( - latitude: 40.7128, - longitude: 74.0060, - altitude: 23.0, - accuracy: 25.0, - verticalAccuracy: 19.0); - ScaffoldMessenger.of(context).showSnackBar(new SnackBar( - content: new Text('Set Last Known Location'), - )); - }, - ), - TextButton( - child: const Text('GET INSTALL TRACKING ID'), - onPressed: () { - _braze.getInstallTrackingId().then((result) { - if (result == null) { - ScaffoldMessenger.of(context).showSnackBar(new SnackBar( - content: new Text("Install Tracking ID was null"), - )); - } else { - ScaffoldMessenger.of(context).showSnackBar(new SnackBar( - content: new Text("Install Tracking ID: " + result), - )); - } - }); - }, - ), - TextButton( - child: const Text('SET GOOGLE ADVERTISING ID'), - onPressed: () { - _braze.setGoogleAdvertisingId("dummy-id", false); - ScaffoldMessenger.of(context).showSnackBar(new SnackBar( - content: new Text("Set Google Advertising ID."))); - }, - ), + SectionHeader("In-app Messages"), TextButton( child: const Text('SET IN-APP MESSAGE CALLBACK'), onPressed: () { @@ -294,7 +254,7 @@ class BrazeFunctionsState extends State { }, ), TextButton( - child: const Text('SET LISTENER FOR IN-APP MESSAGE STREAM'), + child: const Text('SUBSCRIBE VIA IN-APP MESSAGE STREAM'), onPressed: () { this.setState(() { _iamStreamSubscription = 'ENABLED'; @@ -310,6 +270,7 @@ class BrazeFunctionsState extends State { )); }, ), + SectionHeader("Content Cards"), TextButton( child: const Text('REFRESH CONTENT CARDS'), onPressed: () { @@ -337,7 +298,7 @@ class BrazeFunctionsState extends State { }, ), TextButton( - child: const Text('SET LISTENER FOR CONTENT CARDS STREAM'), + child: const Text('SUBSCRIBE VIA CONTENT CARDS STREAM'), onPressed: () { this.setState(() { _ccStreamSubscription = 'ENABLED'; @@ -353,6 +314,48 @@ class BrazeFunctionsState extends State { )); }, ), + SectionHeader("Other"), + TextButton( + child: const Text('SET LAST KNOWN LOCATION'), + onPressed: () { + print( + 'Requesting location initialization (no-op on iOS) and setting last known location'); + _braze.requestLocationInitialization(); + _braze.setLastKnownLocation( + latitude: 40.7128, + longitude: 74.0060, + altitude: 23.0, + accuracy: 25.0, + verticalAccuracy: 19.0); + ScaffoldMessenger.of(context).showSnackBar(new SnackBar( + content: new Text('Set Last Known Location'), + )); + }, + ), + TextButton( + child: const Text('GET INSTALL TRACKING ID'), + onPressed: () { + _braze.getInstallTrackingId().then((result) { + if (result == null) { + ScaffoldMessenger.of(context).showSnackBar(new SnackBar( + content: new Text("Install Tracking ID was null"), + )); + } else { + ScaffoldMessenger.of(context).showSnackBar(new SnackBar( + content: new Text("Install Tracking ID: " + result), + )); + } + }); + }, + ), + TextButton( + child: const Text('SET GOOGLE ADVERTISING ID'), + onPressed: () { + _braze.setGoogleAdvertisingId("dummy-id", false); + ScaffoldMessenger.of(context).showSnackBar(new SnackBar( + content: new Text("Set Google Advertising ID."))); + }, + ), TextButton( child: const Text('WIPE DATA'), onPressed: () { @@ -436,21 +439,28 @@ class BrazeFunctionsState extends State { ); } - void _inAppMessageReceived(BrazeInAppMessage inAppMessage, {String prefix}) { + void _inAppMessageReceived(BrazeInAppMessage inAppMessage, + {String prefix, bool automaticallyInteract = false}) { print("[$prefix] Received message: ${inAppMessage.toString()}"); - _braze.logInAppMessageImpression(inAppMessage); ScaffoldMessenger.of(context).showSnackBar(new SnackBar( - content: new Text( - "[$prefix] Received message and logging clicks: ${inAppMessage.toString()}"), + content: + new Text("[$prefix] Received message: ${inAppMessage.toString()}"), )); - _braze.logInAppMessageClicked(inAppMessage); - inAppMessage.buttons.forEach((button) { - _braze.logInAppMessageButtonClicked(inAppMessage, button.id); - }); + + // Programmatically log impression, body click, and any button clicks + if (automaticallyInteract) { + print( + "[$prefix] Logging impression, body click, and button clicks programmatically."); + _braze.logInAppMessageImpression(inAppMessage); + _braze.logInAppMessageClicked(inAppMessage); + inAppMessage.buttons.forEach((button) { + _braze.logInAppMessageButtonClicked(inAppMessage, button.id); + }); + } } void _contentCardsReceived(List contentCards, - {String prefix}) { + {String prefix, bool automaticallyInteract = false}) { if (contentCards.isEmpty) { ScaffoldMessenger.of(context).showSnackBar(new SnackBar( content: new Text("Empty Content Cards update received."), @@ -459,11 +469,20 @@ class BrazeFunctionsState extends State { } contentCards.forEach((contentCard) { print("[$prefix] Received card: " + contentCard.toString()); - _braze.logContentCardClicked(contentCard); - _braze.logContentCardImpression(contentCard); ScaffoldMessenger.of(context).showSnackBar(new SnackBar( content: new Text("[$prefix] Received card: ${contentCard.toString()}"), )); + + // Programmatically log impression, card click, and dismissal + if (automaticallyInteract) { + print("[$prefix] Logging impression and body click programmatically."); + _braze.logContentCardImpression(contentCard); + _braze.logContentCardClicked(contentCard); + + // Only for testing, remove from actual branch + // - Executes dismissal and removes from UI too + _braze.logContentCardDismissed(contentCard); + } }); } @@ -514,3 +533,30 @@ class BrazeFunctionsState extends State { )); } } + +class SectionHeader extends StatelessWidget { + SectionHeader(this.title); + + final String title; + + @override + Widget build(BuildContext context) { + return Column(children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Divider( + thickness: 2, + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + title, + style: Theme.of(context).textTheme.headline6.copyWith( + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline), + ), + ), + ]); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 66e99ca..b5a79f8 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,7 +1,7 @@ name: braze_plugin_example description: Demonstrates how to use the braze_plugin plugin. publish_to: 'none' -version: 0.0.1 +version: 1.0.0 environment: sdk: ">=2.0.0 <3.0.0" diff --git a/ios/Classes/BrazePlugin.swift b/ios/Classes/BrazePlugin.swift index 9dc6a68..8287ef9 100644 --- a/ios/Classes/BrazePlugin.swift +++ b/ios/Classes/BrazePlugin.swift @@ -1,10 +1,16 @@ -import Appboy_iOS_SDK +import BrazeKit +import BrazeUI import Flutter /// Stores all channels, including ones across different BrazePlugin instances var channels = [FlutterMethodChannel]() -public class BrazePlugin: NSObject, FlutterPlugin, ABKSdkAuthenticationDelegate { +public class BrazePlugin: NSObject, FlutterPlugin, BrazeDelegate { + + public static var braze: Braze? = nil + + private static var inAppMessageIdsToContexts: [String: Braze.InAppMessage.Context] = [:] + private static var contentCardIdsToContexts: [String: Braze.ContentCard.Context] = [:] public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "braze_plugin", binaryMessenger: registrar.messenger()) @@ -14,490 +20,499 @@ public class BrazePlugin: NSObject, FlutterPlugin, ABKSdkAuthenticationDelegate } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let argsDescription = String(describing: call.arguments) switch call.method { case "changeUser": - guard let callArguments = call.arguments as? [String: Any], - let userId = callArguments["userId"] as? String + guard let args = call.arguments as? [String: Any], + let userId = args["userId"] as? String else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + print("Invalid args: \(argsDescription), iOS method: \(call.method)") return } - if Array(callArguments.keys).contains("sdkAuthSignature") { - if let sdkAuthSignature = callArguments["sdkAuthSignature"] as? String { - Appboy.sharedInstance()?.changeUser(userId, sdkAuthSignature: sdkAuthSignature) - return - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))" - ) + if Array(args.keys).contains("sdkAuthSignature") { + guard let sdkAuthSignature = args["sdkAuthSignature"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") return } + BrazePlugin.braze?.changeUser(userId: userId, sdkAuthSignature: sdkAuthSignature) + } else { + BrazePlugin.braze?.changeUser(userId: userId) } - Appboy.sharedInstance()?.changeUser(userId) + case "setSdkAuthenticationSignature": - if let callArguments = call.arguments as? [String: Any], - Array(callArguments.keys).contains("sdkAuthSignature"), - let sdkAuthSignature = callArguments["sdkAuthSignature"] as? String - { - Appboy.sharedInstance()?.setSdkAuthenticationSignature(sdkAuthSignature) - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + Array(args.keys).contains("sdkAuthSignature"), + let sdkAuthSignature = args["sdkAuthSignature"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.braze?.set(sdkAuthenticationSignature: sdkAuthSignature) + case "setSdkAuthenticationDelegate": - Appboy.sharedInstance()?.sdkAuthenticationDelegate = self + // This delegate is only implemented to handle SDK Auth in this plugin + BrazePlugin.braze?.delegate = self + case "getInstallTrackingId": - let deviceId = Appboy.sharedInstance()?.getDeviceId() - result(deviceId) + BrazePlugin.braze?.deviceId { result($0) } + case "requestContentCardsRefresh": - Appboy.sharedInstance()?.requestContentCardsRefresh() + BrazePlugin.braze?.contentCards.requestRefresh { _ in } + case "launchContentCards": - let contentCardsModal = ABKContentCardsViewController() - contentCardsModal.navigationItem.title = "Content Cards" - if let keyWindow = UIApplication.shared.keyWindow, - let mainViewController = keyWindow.rootViewController - { - mainViewController.present(contentCardsModal, animated: true, completion: nil) - } + guard let braze = BrazePlugin.braze, + let mainViewController = UIApplication.shared.keyWindow?.rootViewController + else { return } + let modalViewController = BrazeContentCardUI.ModalViewController(braze: braze) + modalViewController.navigationItem.title = "Content Cards" + mainViewController.present(modalViewController, animated: true) + case "logContentCardClicked": - if let callArguments = call.arguments as? [String: Any], - let contentCardJSONString = callArguments["contentCardString"] as? String - { - let contentCard = ABKContentCard() - BrazePlugin.getContentCardFromString(contentCardJSONString, contentCard: contentCard) - contentCard.logContentCardClicked() - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let contentCardJSONString = args["contentCardString"] as? String, + let braze = BrazePlugin.braze + else { + print("Invalid args: \(argsDescription), braze: \(String(describing: braze)), iOS method: \(call.method)") + return } + if let contentCard = BrazePlugin.contentCard(from: contentCardJSONString, braze: braze) { + contentCard.logClick(using: braze) + } + case "logContentCardDismissed": - if let callArguments = call.arguments as? [String: Any], - let contentCardJSONString = callArguments["contentCardString"] as? String - { - let contentCard = ABKContentCard() - BrazePlugin.getContentCardFromString(contentCardJSONString, contentCard: contentCard) - contentCard.logContentCardDismissed() - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let contentCardJSONString = args["contentCardString"] as? String, + let braze = BrazePlugin.braze + else { + print("Invalid args: \(argsDescription), braze: \(String(describing: braze)), iOS method: \(call.method)") + return + } + if let contentCard = BrazePlugin.contentCard(from: contentCardJSONString, braze: braze) { + contentCard.logDismissed(using: braze) } + case "logContentCardImpression": - if let callArguments = call.arguments as? [String: Any], - let contentCardJSONString = callArguments["contentCardString"] as? String - { - let contentCard = ABKContentCard() - BrazePlugin.getContentCardFromString(contentCardJSONString, contentCard: contentCard) - contentCard.logContentCardImpression() - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let contentCardJSONString = args["contentCardString"] as? String, + let braze = BrazePlugin.braze + else { + print("Invalid args: \(argsDescription), braze: \(String(describing: braze)), iOS method: \(call.method)") + return } + if let contentCard = BrazePlugin.contentCard(from: contentCardJSONString, braze: braze) { + contentCard.logImpression(using: braze) + } + case "logInAppMessageClicked": - if let callArguments = call.arguments as? [String: Any], - let inAppMessageJSONString = callArguments["inAppMessageString"] as? String - { - let inAppMessage = ABKInAppMessage() - BrazePlugin.getInAppMessageFromString(inAppMessageJSONString, inAppMessage: inAppMessage) - inAppMessage.logInAppMessageClicked() - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let inAppMessageJSONString = args["inAppMessageString"] as? String, + let braze = BrazePlugin.braze + else { + print("Invalid args: \(argsDescription), braze: \(String(describing: braze)), iOS method: \(call.method)") + return + } + if let inAppMessage = BrazePlugin.inAppMessage(from: inAppMessageJSONString, braze: braze) { + inAppMessage.logClick(buttonId: nil, using: braze) } + case "logInAppMessageImpression": - if let callArguments = call.arguments as? [String: Any], - let inAppMessageJSONString = callArguments["inAppMessageString"] as? String - { - let inAppMessage = ABKInAppMessage() - BrazePlugin.getInAppMessageFromString(inAppMessageJSONString, inAppMessage: inAppMessage) - inAppMessage.logInAppMessageImpression() - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let inAppMessageJSONString = args["inAppMessageString"] as? String, + let braze = BrazePlugin.braze + else { + print("Invalid args: \(argsDescription), braze: \(String(describing: braze)), iOS method: \(call.method)") + return } + if let inAppMessage = BrazePlugin.inAppMessage(from: inAppMessageJSONString, braze: braze) { + inAppMessage.logImpression(using: braze) + } + case "logInAppMessageButtonClicked": - if let callArguments = call.arguments as? [String: Any], - let inAppMessageJSONString = callArguments["inAppMessageString"] as? String, - let idNumber = callArguments["buttonId"] as? NSNumber - { - let inAppMessageImmersive = ABKInAppMessageImmersive() - BrazePlugin.getInAppMessageFromString( - inAppMessageJSONString, inAppMessage: inAppMessageImmersive) - inAppMessageImmersive.logInAppMessageClicked(withButtonID: idNumber.intValue) - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let inAppMessageJSONString = args["inAppMessageString"] as? String, + let idNumber = args["buttonId"] as? NSNumber, + let braze = BrazePlugin.braze + else { + print("Invalid args: \(argsDescription), braze: \(String(describing: braze)), iOS method: \(call.method)") + return + } + if let inAppMessage = BrazePlugin.inAppMessage(from: inAppMessageJSONString, braze: braze) { + inAppMessage.logClick(buttonId: idNumber.stringValue, using: braze) } + case "addAlias": - if let callArguments = call.arguments as? [String: Any], - let aliasName = callArguments["aliasName"] as? String, - let aliasLabel = callArguments["aliasLabel"] as? String - { - Appboy.sharedInstance()?.user.addAlias(aliasName, withLabel: aliasLabel) - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let aliasName = args["aliasName"] as? String, + let aliasLabel = args["aliasLabel"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.braze?.user.add(alias: aliasName, label: aliasLabel) + case "logCustomEvent", "logCustomEventWithProperties": - if let callArguments = call.arguments as? [String: Any], - let eventName = callArguments["eventName"] as? String - { - Appboy.sharedInstance()?.sdkFlavor = .FLUTTER - Appboy.sharedInstance()?.addSdkMetadata([ABKSdkMetadata.flutter]) - if let properties = callArguments["properties"] as? [AnyHashable: Any] { - Appboy.sharedInstance()?.logCustomEvent(eventName, withProperties: properties) - } else { - Appboy.sharedInstance()?.logCustomEvent(eventName) - } - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let eventName = args["eventName"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + let properties = args["properties"] as? [String: Any] + BrazePlugin.braze?.logCustomEvent(name: eventName, properties: properties) + case "logPurchase", "logPurchaseWithProperties": - if let callArguments = call.arguments as? [String: Any], - let productId = callArguments["productId"] as? String, - let currencyCode = callArguments["currencyCode"] as? String, - let priceNumber = callArguments["price"] as? NSNumber, - let quantity = callArguments["quantity"] as? NSNumber - { - let price = priceNumber.decimalValue as NSDecimalNumber - if let properties = callArguments["properties"] as? [AnyHashable: Any] { - Appboy.sharedInstance()?.logPurchase( - productId, inCurrency: currencyCode, atPrice: price, withQuantity: quantity.uintValue, - andProperties: properties) - } else { - Appboy.sharedInstance()?.logPurchase( - productId, inCurrency: currencyCode, atPrice: price, withQuantity: quantity.uintValue - ) - } - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))" - ) + guard let args = call.arguments as? [String: Any], + let productId = args["productId"] as? String, + let currencyCode = args["currencyCode"] as? String, + let price = args["price"] as? Double, + let quantity = args["quantity"] as? NSNumber + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + let properties = args["properties"] as? [String: Any] + BrazePlugin.braze?.logPurchase( + productId: productId, + currency: currencyCode, + price: price, + quantity: quantity.intValue, + properties: properties + ) + case "setFirstName": - if let callArguments = call.arguments as? [String: Any], - let firstName = callArguments["firstName"] as? String - { - Appboy.sharedInstance()?.user.firstName = firstName - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let firstName = args["firstName"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.braze?.user.set(firstName: firstName) + case "setLastName": - if let callArguments = call.arguments as? [String: Any], - let lastName = callArguments["lastName"] as? String - { - Appboy.sharedInstance()?.user.lastName = lastName - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let lastName = args["lastName"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.braze?.user.set(lastName: lastName) + case "setLanguage": - if let callArguments = call.arguments as? [String: Any], - let language = callArguments["language"] as? String - { - Appboy.sharedInstance()?.user.language = language - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let language = args["language"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.braze?.user.set(language: language) + case "setCountry": - if let callArguments = call.arguments as? [String: Any], - let country = callArguments["country"] as? String - { - Appboy.sharedInstance()?.user.country = country - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let country = args["country"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.braze?.user.set(country: country) + case "setGender": - if let callArguments = call.arguments as? [String: Any], - let gender = callArguments["gender"] as? String - { - BrazePlugin.setGender(gender) - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let gender = args["gender"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.setGender(gender) + case "setHomeCity": - if let callArguments = call.arguments as? [String: Any], - let homeCity = callArguments["homeCity"] as? String - { - Appboy.sharedInstance()?.user.homeCity = homeCity - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let homeCity = args["homeCity"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.braze?.user.set(homeCity: homeCity) + case "setDateOfBirth": - if let callArguments = call.arguments as? [String: Any], - let day = callArguments["day"] as? NSNumber, - let month = callArguments["month"] as? NSNumber, - let year = callArguments["year"] as? NSNumber - { - let calendar = Calendar.current - var components = DateComponents() - components.setValue(day.intValue, for: .day) - components.setValue(month.intValue, for: .month) - components.setValue(year.intValue, for: .year) - let dateOfBirth = calendar.date(from: components) - Appboy.sharedInstance()?.user.dateOfBirth = dateOfBirth - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let day = args["day"] as? NSNumber, + let month = args["month"] as? NSNumber, + let year = args["year"] as? NSNumber + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + let calendar = Calendar.current + var components = DateComponents() + components.setValue(day.intValue, for: .day) + components.setValue(month.intValue, for: .month) + components.setValue(year.intValue, for: .year) + let dateOfBirth = calendar.date(from: components) + BrazePlugin.braze?.user.set(dateOfBirth: dateOfBirth) + case "setEmail": if let callArguments = call.arguments as? [String: Any], let email = callArguments["email"] as? String { - Appboy.sharedInstance()?.user.email = email + BrazePlugin.braze?.user.set(email: email) } else { - Appboy.sharedInstance()?.user.email = nil + BrazePlugin.braze?.user.set(email: nil) } + case "setPhoneNumber": - if let callArguments = call.arguments as? [String: Any], - let phoneNumber = callArguments["phoneNumber"] as? String - { - Appboy.sharedInstance()?.user.phone = phoneNumber - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let phoneNumber = args["phoneNumber"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.braze?.user.set(phoneNumber: phoneNumber) + case "setPushNotificationSubscriptionType": - if let callArguments = call.arguments as? [String: Any], - let type = callArguments["type"] as? String - { - let pushNotificationSubscriptionType = BrazePlugin.getSubscriptionType(type) - Appboy.sharedInstance()?.user.setPush(pushNotificationSubscriptionType) - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let type = args["type"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + let pushNotificationSubscriptionType = BrazePlugin.getSubscriptionType(type) + BrazePlugin.braze?.user.set( + pushNotificationSubscriptionState: pushNotificationSubscriptionType) + case "setEmailNotificationSubscriptionType": - if let callArguments = call.arguments as? [String: Any], - let type = callArguments["type"] as? String - { - let emailNotificationSubscriptionType = BrazePlugin.getSubscriptionType(type) - Appboy.sharedInstance()?.user.setEmailNotificationSubscriptionType( - emailNotificationSubscriptionType) - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let type = args["type"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + let subscriptionType = BrazePlugin.getSubscriptionType(type) + BrazePlugin.braze?.user.set(emailSubscriptionState: subscriptionType) + case "addToSubscriptionGroup": - if let callArguments = call.arguments as? [String: Any], - let groupId = callArguments["groupId"] as? String - { - Appboy.sharedInstance()?.user.addToSubscriptionGroup(withGroupId: groupId) - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let groupId = args["groupId"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.braze?.user.addToSubscriptionGroup(id: groupId) + case "removeFromSubscriptionGroup": - if let callArguments = call.arguments as? [String: Any], - let groupId = callArguments["groupId"] as? String - { - Appboy.sharedInstance()?.user.removeFromSubscriptionGroup(withGroupId: groupId) - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let groupId = args["groupId"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.braze?.user.removeFromSubscriptionGroup(id: groupId) + case "setStringCustomUserAttribute": - if let callArguments = call.arguments as? [String: Any], - let key = callArguments["key"] as? String, - let value = callArguments["value"] as? String - { - Appboy.sharedInstance()?.user.setCustomAttributeWithKey(key, andStringValue: value) - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let key = args["key"] as? String, + let value = args["value"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.braze?.user.setCustomAttribute(key: key, value: value) + case "setIntCustomUserAttribute": - if let callArguments = call.arguments as? [String: Any], - let key = callArguments["key"] as? String, - let value = callArguments["value"] as? NSNumber - { - Appboy.sharedInstance()?.user.setCustomAttributeWithKey( - key, andIntegerValue: value.intValue) - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let key = args["key"] as? String, + let value = args["value"] as? NSNumber + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.braze?.user.setCustomAttribute(key: key, value: value.intValue) + case "setDoubleCustomUserAttribute": - if let callArguments = call.arguments as? [String: Any], - let key = callArguments["key"] as? String, - let value = callArguments["value"] as? NSNumber - { - Appboy.sharedInstance()?.user.setCustomAttributeWithKey( - key, andDoubleValue: value.doubleValue) - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let key = args["key"] as? String, + let value = args["value"] as? NSNumber + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.braze?.user.setCustomAttribute(key: key, value: value.doubleValue) + case "setBoolCustomUserAttribute": - if let callArguments = call.arguments as? [String: Any], - let key = callArguments["key"] as? String, - let value = callArguments["value"] as? Bool - { - Appboy.sharedInstance()?.user.setCustomAttributeWithKey(key, andBOOLValue: value) - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let key = args["key"] as? String, + let value = args["value"] as? Bool + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.braze?.user.setCustomAttribute(key: key, value: value) + case "setDateCustomUserAttribute": - if let callArguments = call.arguments as? [String: Any], - let key = callArguments["key"] as? String, - let value = callArguments["value"] as? NSNumber - { - let date = Date.init(timeIntervalSince1970: value.doubleValue) - Appboy.sharedInstance()?.user.setCustomAttributeWithKey(key, andDateValue: date) - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let key = args["key"] as? String, + let value = args["value"] as? NSNumber + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + let date = Date.init(timeIntervalSince1970: value.doubleValue) + BrazePlugin.braze?.user.setCustomAttribute(key: key, value: date) + case "setLocationCustomAttribute": - if let callArguments = call.arguments as? [String: Any], - let key = callArguments["key"] as? String, - let lat = callArguments["lat"] as? NSNumber, - let longitude = callArguments["long"] as? NSNumber - { - Appboy.sharedInstance()?.user.addLocationCustomAttribute( - withKey: key, latitude: lat.doubleValue, longitude: longitude.doubleValue) - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let key = args["key"] as? String, + let lat = args["lat"] as? NSNumber, + let longitude = args["long"] as? NSNumber + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.braze?.user.setLocationCustomAttribute( + key: key, latitude: lat.doubleValue, longitude: longitude.doubleValue) + case "addToCustomAttributeArray": - if let callArguments = call.arguments as? [String: Any], - let key = callArguments["key"] as? String, - let value = callArguments["value"] as? String - { - Appboy.sharedInstance()?.user.addToCustomAttributeArray(withKey: key, value: value) - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let key = args["key"] as? String, + let value = args["value"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.braze?.user.addToCustomAttributeArray(key: key, value: value) + case "removeFromCustomAttributeArray": - if let callArguments = call.arguments as? [String: Any], - let key = callArguments["key"] as? String, - let value = callArguments["value"] as? String - { - Appboy.sharedInstance()?.user.removeFromCustomAttributeArray(withKey: key, value: value) - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let key = args["key"] as? String, + let value = args["value"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.braze?.user.removeFromCustomAttributeArray(key: key, value: value) + case "incrementCustomUserAttribute": - if let callArguments = call.arguments as? [String: Any], - let key = callArguments["key"] as? String, - let value = callArguments["value"] as? NSNumber - { - Appboy.sharedInstance()?.user.incrementCustomUserAttribute(key, by: value.intValue) - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let key = args["key"] as? String, + let value = args["value"] as? NSNumber + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + BrazePlugin.braze?.user.incrementCustomUserAttribute(key: key, by: value.intValue) + case "unsetCustomUserAttribute": - if let callArguments = call.arguments as? [String: Any], - let key = callArguments["key"] as? String - { - Appboy.sharedInstance()?.user.unsetCustomAttribute(withKey: key) - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let key = args["key"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } - case "registerAndroidPushToken": - break // This is an Android only feature, do nothing. - case "setGoogleAdvertisingId": - break // This is an Android only feature, do nothing. + BrazePlugin.braze?.user.unsetCustomAttribute(key: key) + + case "registerAndroidPushToken", "setGoogleAdvertisingId": + break // Android-only features, do nothing. + case "requestImmediateDataFlush": - Appboy.sharedInstance()?.requestImmediateDataFlush() + BrazePlugin.braze?.requestImmediateDataFlush() + case "setAttributionData": - if let callArguments = call.arguments as? [String: Any], - let network = callArguments["network"] as? String, - let campaign = callArguments["campaign"] as? String, - let adGroup = callArguments["adGroup"] as? String, - let creative = callArguments["creative"] as? String - { - let attributionData = ABKAttributionData( - network: network, campaign: campaign, adGroup: adGroup, creative: creative) - Appboy.sharedInstance()?.user.attributionData = attributionData - } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + guard let args = call.arguments as? [String: Any], + let network = args["network"] as? String, + let campaign = args["campaign"] as? String, + let adGroup = args["adGroup"] as? String, + let creative = args["creative"] as? String + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return } + let attributionData = Braze.User.AttributionData( + network: network, campaign: campaign, adGroup: adGroup, creative: creative) + BrazePlugin.braze?.user.set(attributionData: attributionData) + case "wipeData": - Appboy.wipeDataAndDisableForAppRun() + BrazePlugin.braze?.wipeData() + case "requestLocationInitialization": break // This is an Android only feature, do nothing. + case "setLastKnownLocation": - if let callArguments = call.arguments as? [String: Any], - let latitude = callArguments["latitude"] as? Double, - let longitude = callArguments["longitude"] as? Double, - let accuracy = callArguments["accuracy"] as? Double + guard let args = call.arguments as? [String: Any], + let latitude = args["latitude"] as? Double, + let longitude = args["longitude"] as? Double, + let accuracy = args["accuracy"] as? Double + else { + print("Invalid args: \(argsDescription), iOS method: \(call.method)") + return + } + if let altitude = args["altitude"] as? Double, + let verticalAccuracy = args["verticalAccuracy"] as? Double, + verticalAccuracy > 0.0 { - if let altitude = callArguments["altitude"] as? NSNumber, - let verticalAccuracy = callArguments["verticalAccuracy"] as? NSNumber, - verticalAccuracy.doubleValue > 0.0 - { - Appboy.sharedInstance()?.user.setLastKnownLocationWithLatitude( - latitude, longitude: longitude, horizontalAccuracy: accuracy, - altitude: altitude.doubleValue, verticalAccuracy: verticalAccuracy.doubleValue) - } else { - Appboy.sharedInstance()?.user.setLastKnownLocationWithLatitude( - latitude, longitude: longitude, horizontalAccuracy: accuracy) - } + BrazePlugin.braze?.user.setLastKnownLocation( + latitude: latitude, + longitude: longitude, + altitude: altitude, + horizontalAccuracy: accuracy, + verticalAccuracy: verticalAccuracy + ) } else { - print( - "Invalid arguments for \(call.method) iOS method: \(String(describing: call.arguments))") + BrazePlugin.braze?.user.setLastKnownLocation( + latitude: latitude, + longitude: longitude, + horizontalAccuracy: accuracy + ) } + case "enableSDK": - Appboy.requestEnableSDKOnNextAppRun() + BrazePlugin.braze?.enabled = true case "disableSDK": - Appboy.disableSDK() + BrazePlugin.braze?.enabled = false + default: result(FlutterMethodNotImplemented) } } - private class func getInAppMessageFromString( - _ inAppMessageJSONString: String, inAppMessage: ABKInAppMessage - ) { - if let inAppMessageData = inAppMessageJSONString.data( - using: String.Encoding.utf8, allowLossyConversion: false) - { - do { - if let deserializedInAppMessageDict = try JSONSerialization.jsonObject( - with: inAppMessageData, options: .mutableContainers) as? [String: Any] - { - inAppMessage.setValuesForKeys(deserializedInAppMessageDict) - } - } catch let error as NSError { - print(error.localizedDescription) - } + private class func inAppMessage(from jsonString: String, braze: BrazeKit.Braze) -> Braze.InAppMessage? { + let inAppMessageRaw = try? JSONDecoder().decode( + Braze.InAppMessageRaw.self, from: Data(jsonString.utf8)) + guard let inAppMessageRaw = inAppMessageRaw else { return nil } + + do { + var inAppMessage: Braze.InAppMessage = try Braze.InAppMessage.init(inAppMessageRaw) + + // TODO: New context is being allocated each time, so can log duplicate impressions + let context = Braze.InAppMessage.Context(message: inAppMessage, using: braze) + inAppMessage.context = context + + return inAppMessage + } catch { + print("Error parsing in-app message from jsonString: \(jsonString), error: \(error)") } + return nil } - private class func getContentCardFromString( - _ contentCardJSONString: String, contentCard: ABKContentCard - ) { - if let contentCardData = contentCardJSONString.data( - using: String.Encoding.utf8, allowLossyConversion: false) - { - do { - if let deserializedContentCardDict = try JSONSerialization.jsonObject( - with: contentCardData, options: .mutableContainers) as? [String: Any] - { - contentCard.setValuesForKeys(deserializedContentCardDict) - } - } catch let error as NSError { - print(error.localizedDescription) - } + private class func contentCard(from jsonString: String, braze: BrazeKit.Braze) -> Braze.ContentCard? { + let contentCardRaw = Braze.ContentCardRaw.from(json: Data(jsonString.utf8)) + guard let contentCardRaw = contentCardRaw else { return nil } + + do { + var contentCard: Braze.ContentCard = try Braze.ContentCard.init(contentCardRaw) + + // TODO: New context is being allocated each time, so can log duplicate impressions + let context = Braze.ContentCard.Context(card: contentCard, using: braze) + contentCard.context = context + + return contentCard + } catch { + print("Error parsing Content Card from jsonString: \(jsonString), error: \(error)") } + return nil } private class func getSubscriptionType(_ subscriptionValue: String) - -> ABKNotificationSubscriptionType + -> Braze.User.SubscriptionState { switch subscriptionValue { case "SubscriptionType.unsubscribed": @@ -513,10 +528,10 @@ public class BrazePlugin: NSObject, FlutterPlugin, ABKSdkAuthenticationDelegate private class func setGender(_ gender: String) { let genderInputType = parseUserGenderInput(gender) - Appboy.sharedInstance()?.user.setGender(genderInputType) + BrazePlugin.braze?.user.set(gender: genderInputType) } - private class func parseUserGenderInput(_ gender: String) -> ABKUserGenderType { + private class func parseUserGenderInput(_ gender: String) -> Braze.User.Gender { switch gender.uppercased().prefix(1) { case "F": return .female @@ -537,41 +552,61 @@ public class BrazePlugin: NSObject, FlutterPlugin, ABKSdkAuthenticationDelegate // MARK: - Public methods - public class func processInAppMessage(_ inAppMessage: ABKInAppMessage) { - guard let inAppMessageData = inAppMessage.serializeToData(), + /// The intialization method to create a Braze instance. + /// Call this method in your AppDelegate `didFinishLaunching` method. + public class func initBraze(_ configuration: Braze.Configuration) -> Braze { + configuration.api.addSDKMetadata([.flutter]) + configuration.api.sdkFlavor = .flutter + let braze = Braze(configuration: configuration) + BrazePlugin.braze = braze + return braze + } + + /// Translates the native [inAppMessage] into JSON and passes it from the iOS layer + /// to the Dart layer. + /// Note: Swift closures are unable to be translated into JSON. + public class func processInAppMessage(_ inAppMessage: Braze.InAppMessage) { + guard let inAppMessageData = inAppMessage.json(), let inAppMessageString = String(data: inAppMessageData, encoding: .utf8) else { print("Invalid inAppMessage: \(inAppMessage)") return } - let arguments = ["inAppMessage": inAppMessageString] + let arguments = ["inAppMessage": inAppMessageString] for channel in channels { channel.invokeMethod("handleBrazeInAppMessage", arguments: arguments) } } - public class func processContentCards(_ cards: [ABKContentCard]) { + /// Translates each of the the native content [cards] into JSON and passes it + /// from the iOS layer to the Dart layer. + /// Note: Swift closures are unable to be translated into JSON. + public class func processContentCards(_ cards: [Braze.ContentCard]) { var cardStrings: [String] = [] for card in cards { - if let cardData = card.serializeToData(), + if let cardData = card.json(), let cardString = String(data: cardData, encoding: .utf8) { cardStrings.append(cardString) } else { - print("Invalid content card: \(card)") + print("Invalid content card: \(card). Skipping card.") } } - let arguments = ["contentCards": cardStrings] + let arguments = ["contentCards": cardStrings] for channel in channels { channel.invokeMethod("handleBrazeContentCards", arguments: arguments) } } - // MARK: - ABKSdkAuthenticationDelegate + // MARK: SDK Authentication - public func handle(_ authError: ABKSdkAuthenticationError) { + public func braze( + _ braze: BrazeKit.Braze, + sdkAuthenticationFailedWithError error: BrazeKit.Braze.SDKAuthenticationError + ) { + let authError = error let dictionary: [String: Any?] = [ "code": authError.code, "reason": authError.reason, diff --git a/ios/braze_plugin.podspec b/ios/braze_plugin.podspec index bdc5d87..cf9348e 100644 --- a/ios/braze_plugin.podspec +++ b/ios/braze_plugin.podspec @@ -3,19 +3,18 @@ # Pod::Spec.new do |s| s.name = 'braze_plugin' - s.version = '2.6.1' + s.version = '3.0.0' s.summary = 'Braze plugin for Flutter.' - s.description = <<-DESC -Braze plugin for Flutter. - DESC - s.homepage = 'http://example.com' + s.homepage = 'https://braze.com' s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } + s.authors = 'Braze, Inc.' s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.dependency 'Appboy-iOS-SDK', '~> 4.5.1' + s.dependency 'BrazeKit', '~> 5.6.4' + s.dependency 'BrazeLocation', '~> 5.6.4' + s.dependency 'BrazeUI', '~> 5.6.4' - s.ios.deployment_target = '9.0' + s.ios.deployment_target = '10.0' end diff --git a/lib/braze_plugin.dart b/lib/braze_plugin.dart index 96f2a85..904b6a2 100644 --- a/lib/braze_plugin.dart +++ b/lib/braze_plugin.dart @@ -41,7 +41,7 @@ class BrazePlugin { /// Subscribes to the stream of in-app messages and calls [onEvent] when it /// receives an in-app message. StreamSubscription subscribeToInAppMessages( - Function onEvent(BrazeInAppMessage inAppMessage)) { + void Function(BrazeInAppMessage) onEvent) { if (_replayCallbacksConfigEnabled() && _queuedInAppMessages.isNotEmpty) { print( "Replaying stream onEvent for previously queued Braze in-app messages."); @@ -57,7 +57,7 @@ class BrazePlugin { /// Subscribes to the stream of content cards and calls [onEvent] when it /// receives the list of content cards. StreamSubscription subscribeToContentCards( - Function onEvent(List contentCard)) { + void Function(List) onEvent) { if (_replayCallbacksConfigEnabled() && _queuedContentCards.isNotEmpty) { print( "Replaying stream onEvent for previously queued Braze content cards."); @@ -657,6 +657,9 @@ class BrazeContentCard { /// Content Card viewed bool viewed = false; + /// Content Card control + bool isControl = false; + BrazeContentCard(String _data) { contentCardJsonString = _data; var contentCardJson = json.jsonDecode(_data); @@ -721,6 +724,9 @@ class BrazeContentCard { if (typeJson is String) { type = typeJson; } + if (type == "control") { + isControl = true; + } var urlJson = contentCardJson["u"]; if (urlJson is String) { url = urlJson; @@ -773,6 +779,8 @@ class BrazeContentCard { expiresAt.toString() + " dismissable:" + dismissable.toString() + + " isControl:" + + isControl.toString() + " contentCardJsonString:" + contentCardJsonString; } diff --git a/pubspec.yaml b/pubspec.yaml index 70020e8..c0b59d9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: braze_plugin description: This is the Braze plugin for Flutter. Effective marketing automation is an essential part of successfully scaling and managing your business. -version: 2.6.1 +version: 3.0.0 homepage: https://www.braze.com/ repository: https://github.com/braze-inc/braze-flutter-sdk diff --git a/test/braze_plugin_test.dart b/test/braze_plugin_test.dart index 09af077..176507e 100644 --- a/test/braze_plugin_test.dart +++ b/test/braze_plugin_test.dart @@ -99,6 +99,13 @@ void main() { ]); }); + test('should include isControl field', () { + BrazePlugin _braze = new BrazePlugin(); + String _data = '{"tp":"control"}'; + BrazeContentCard _contentCard = new BrazeContentCard(_data); + expect(_contentCard.isControl, equals(true)); + }); + test('should call logContentCardImpression', () { BrazePlugin _braze = new BrazePlugin(); String _data = '{"someJson":"data"}';