From 4c5f021b53cc1bed3573e02dd03ef16bcce7f7a8 Mon Sep 17 00:00:00 2001 From: Mike Summerfeldt <20338451+IT-MikeS@users.noreply.github.com> Date: Thu, 10 Oct 2024 12:37:02 -0400 Subject: [PATCH] RMET-3677 & RMET-3678 - Add setConsent functionality (#42) * RMET-3608 - Prepare to release version `5.0.0-OS14` (#41) * RMET-3608 FB Analytics - Update dependency to Firebase Analytics Android SDK (#40) * feat: use Firebase Android BOM library to set the version of FB Analytics Context: According to the documentation, this is the recommended way of controlling Firebase library versions. This way, using the Firebase Android BOM, an app with multiple Firebase Android libraries will always use compatible versions of these libraries. References: https://outsystemsrd.atlassian.net/browse/RMET-3608 * fix: declare Firebase dependencies in build.gradle file References: https://outsystemsrd.atlassian.net/browse/RMET-3608 * test: use framework to set firebase-analytics version References: https://outsystemsrd.atlassian.net/browse/RMET-3608 * refactor: declare Firebase dependencies in build.gradle file References: https://outsystemsrd.atlassian.net/browse/RMET-3608 * fix: enable Kotlin Context: This is necessary for MABS 9. We currently say that the current version of the plugin works with MABS 9 and MAS 10, but it actually doesn't work with MABS 9 because we use Kotlin code but do not enable the Kotlin plugin. References: https://outsystemsrd.atlassian.net/browse/RMET-3608 * test: enable AndroidX * chore: revert previous commit * fix: enable Kotlin Context: This is necessary for MABS 9. We currently say that the current version of the plugin works with MABS 9 and MAS 10, but it actually doesn't work with MABS 9 because we use Kotlin code but do not enable the Kotlin plugin. References: https://outsystemsrd.atlassian.net/browse/RMET-3608 * fix: revert previous commit * test: include FB Analytics using framework * test: enable Kotlin * refactor: include FB dependencies in build.gradle References: https://outsystemsrd.atlassian.net/browse/RMET-3608 * chore: revert previous commit that added Kotlin * chore: update changelog https://outsystemsrd.atlassian.net/browse/RMET-3608 * chore(release): raise to version 5.0.0-OS14 References: https://outsystemsrd.atlassian.net/browse/RMET-3608 * feat: ios setConsent * feat(android): add setConsent in android * chore: json parse * chore: fix android impl * chore: missed import * chore: more missing imports * chore: obj-c issues * chore: keep jsonstring * chore: fix array parse ios * chore: correct setConset param type * fix: helper methods * fix: cordova passing odd param * chore: changelog * chore: jsdoc update * chore: fix up * chore: missing import * chore: car clean up * chore: better error and logic handling * chore: typo * chore: enums instead of helper func * chore: clean up --------- Co-authored-by: Ricardo Silva <97543217+OS-ricardomoreirasilva@users.noreply.github.com> Co-authored-by: Alexandre Jacinto --- CHANGELOG.md | 4 + plugin.xml | 2 + .../analytics/FirebaseAnalyticsPlugin.java | 51 +++++++++- .../analytics/model/OSFANLConsentModels.kt | 29 ++++++ src/ios/Common/OSFANLConsentHelper.swift | 96 +++++++++++++++++++ src/ios/FirebaseAnalyticsPlugin.h | 1 + src/ios/FirebaseAnalyticsPlugin.m | 13 ++- www/FirebaseAnalytics.js | 17 ++++ 8 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 src/android/com/outsystems/firebase/analytics/model/OSFANLConsentModels.kt create mode 100644 src/ios/Common/OSFANLConsentHelper.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 9765c5a..cd8e16b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 The changes documented here do not include those from the original repository. +## 5.0.0-OS15 + +- Feat(Android & iOS): Add setConsent functionality to allow users to set consent for analytics (https://outsystemsrd.atlassian.net/browse/RMET-3677 & https://outsystemsrd.atlassian.net/browse/RMET-3678). + ## 5.0.0-OS14 - Android | Update dependency to Firebase Analytics Android library (https://outsystemsrd.atlassian.net/browse/RMET-3608). diff --git a/plugin.xml b/plugin.xml index d48f963..d97db68 100644 --- a/plugin.xml +++ b/plugin.xml @@ -57,6 +57,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" + @@ -108,6 +109,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" + diff --git a/src/android/com/outsystems/firebase/analytics/FirebaseAnalyticsPlugin.java b/src/android/com/outsystems/firebase/analytics/FirebaseAnalyticsPlugin.java index 5bf7f58..83e8200 100644 --- a/src/android/com/outsystems/firebase/analytics/FirebaseAnalyticsPlugin.java +++ b/src/android/com/outsystems/firebase/analytics/FirebaseAnalyticsPlugin.java @@ -9,14 +9,19 @@ import com.google.firebase.analytics.FirebaseAnalytics; import com.outsystems.firebase.analytics.OSFANLManager; +import com.outsystems.firebase.analytics.model.ConsentType; +import com.outsystems.firebase.analytics.model.ConsentStatus; import com.outsystems.firebase.analytics.model.OSFANLError; import com.outsystems.firebase.analytics.model.OSFANLEventOutputModel; import org.apache.cordova.CallbackContext; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.Iterator; +import java.util.HashMap; +import java.util.Map; public class FirebaseAnalyticsPlugin extends ReflectiveCordovaPlugin { @@ -103,6 +108,50 @@ private void logECommerceEvent(JSONObject params, CallbackContext callbackContex } } + @CordovaMethod + private void setConsent(String consentSetting, CallbackContext callbackContext) throws JSONException { + + try { + JSONArray consentSettings = new JSONArray(consentSetting); + + Map consentMap = new HashMap<>(); + + for (int i = 0; i < consentSettings.length(); i++) { + JSONObject consentItem = consentSettings.getJSONObject(i); + int typeValue = consentItem.getInt("Type"); + int statusValue = consentItem.getInt("Status"); + + FirebaseAnalytics.ConsentType consentType = ConsentType.fromInt(typeValue); + FirebaseAnalytics.ConsentStatus consentStatus = ConsentStatus.fromInt(statusValue); + + if (consentType != null) { + if (consentStatus != null) { + if (consentMap.containsKey(consentType)) { + throw OSFANLError.Companion.duplicateItemsIn("ConsentSettings"); + } + consentMap.put(consentType, consentStatus); + } else { + throw OSFANLError.Companion.invalidType("Consent Status of " + consentType, "GRANTED, or DENIED"); + } + } else { + throw OSFANLError.Companion.invalidType("Consent Type", "AD_PERSONALIZATION, AD_STORAGE, AD_USER_DATA, or ANALYTICS_STORAGE"); + } + } + + if (!consentMap.isEmpty()) { + this.firebaseAnalytics.setConsent(consentMap); + callbackContext.success(); + } else { + throw OSFANLError.Companion.missing("ConsentSettings"); + } + } catch (OSFANLError e) { + JSONObject result = new JSONObject(); + result.put("code", e.getCode()); + result.put("message", e.getMessage()); + callbackContext.error(result); + } + } + private static Bundle parse(JSONObject params) throws JSONException { Bundle bundle = new Bundle(); Iterator it = params.keys(); @@ -126,4 +175,4 @@ private static Bundle parse(JSONObject params) throws JSONException { return bundle; } -} +} \ No newline at end of file diff --git a/src/android/com/outsystems/firebase/analytics/model/OSFANLConsentModels.kt b/src/android/com/outsystems/firebase/analytics/model/OSFANLConsentModels.kt new file mode 100644 index 0000000..33ad5c0 --- /dev/null +++ b/src/android/com/outsystems/firebase/analytics/model/OSFANLConsentModels.kt @@ -0,0 +1,29 @@ +package com.outsystems.firebase.analytics.model + +import com.google.firebase.analytics.FirebaseAnalytics + +enum class ConsentType(val value: Int, val consentType: FirebaseAnalytics.ConsentType) { + AD_PERSONALIZATION(1, FirebaseAnalytics.ConsentType.AD_PERSONALIZATION), + AD_STORAGE(2, FirebaseAnalytics.ConsentType.AD_STORAGE), + AD_USER_DATA(3, FirebaseAnalytics.ConsentType.AD_USER_DATA), + ANALYTICS_STORAGE(4, FirebaseAnalytics.ConsentType.ANALYTICS_STORAGE); + + companion object { + private val map = entries.associateBy(ConsentType::value) + + @JvmStatic + fun fromInt(value: Int): FirebaseAnalytics.ConsentType? = map[value]?.consentType + } +} + +enum class ConsentStatus(val value: Int, val consentStatus: FirebaseAnalytics.ConsentStatus) { + GRANTED(1, FirebaseAnalytics.ConsentStatus.GRANTED), + DENIED(2, FirebaseAnalytics.ConsentStatus.DENIED); + + companion object { + private val map = entries.associateBy(ConsentStatus::value) + + @JvmStatic + fun fromInt(value: Int): FirebaseAnalytics.ConsentStatus? = map[value]?.consentStatus + } +} \ No newline at end of file diff --git a/src/ios/Common/OSFANLConsentHelper.swift b/src/ios/Common/OSFANLConsentHelper.swift new file mode 100644 index 0000000..ea845ed --- /dev/null +++ b/src/ios/Common/OSFANLConsentHelper.swift @@ -0,0 +1,96 @@ +import FirebaseAnalytics +import FirebaseCore + +@objc enum ConsentTypeRawValue: Int, CustomStringConvertible, CaseIterable { + case adPersonalization = 1 + case adStorage = 2 + case adUserData = 3 + case analyticsStorage = 4 + + var description: String { + return switch self { + case .adPersonalization: "ad_personalization" + case .adStorage: "ad_storage" + case .adUserData: "ad_user_data" + case .analyticsStorage: "analytics_storage" + } + } + + static func allOptionsString() -> String { + let capitalizedDescriptions = allCases.map { $0.description.uppercased() } + + if capitalizedDescriptions.count > 1 { + let lastOption = capitalizedDescriptions.last! + let allButLast = capitalizedDescriptions.dropLast().joined(separator: ", ") + return "\(allButLast), or \(lastOption)" + } else { + return capitalizedDescriptions.first ?? "" + } + } +} + +@objc enum ConsentStatusRawValue: Int, CustomStringConvertible, CaseIterable { + case granted = 1 + case denied = 2 + + var description: String { + return switch self { + case .granted: "granted" + case .denied: "denied" + } + } + + static func allOptionsString() -> String { + let capitalizedDescriptions = allCases.map { $0.description.uppercased() } + + if capitalizedDescriptions.count > 1 { + let lastOption = capitalizedDescriptions.last! + let allButLast = capitalizedDescriptions.dropLast().joined(separator: ", ") + return "\(allButLast), or \(lastOption)" + } else { + return capitalizedDescriptions.first ?? "" + } + } +} + +@objc class OSFANLConsentHelper: NSObject { + @objc static func createConsentModel(_ commandArguments: NSArray) throws -> [ConsentType: ConsentStatus] { + guard let jsonString = commandArguments[0] as? String, + let jsonData = jsonString.data(using: .utf8), + let array = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [[String: Any]] else { + throw OSFANLError.invalidType("ConsentSettings", type: "JSON") + } + + var firebaseConsentDict: [ConsentType: ConsentStatus] = [:] + + for item in array { + guard let typeRawValue = item["Type"] as? Int, + let statusRawValue = item["Status"] as? Int else { + throw OSFANLError.invalidType("JSON passed Consent Type or Status", type: "Integer") + } + + guard let consentTypeRawValue = ConsentTypeRawValue(rawValue: typeRawValue) else { + throw OSFANLError.invalidType("Consent Type", type: ConsentTypeRawValue.allOptionsString()) + } + + guard let consentStatusRawValue = ConsentStatusRawValue(rawValue: statusRawValue) else { + throw OSFANLError.invalidType("Consent Status", type: ConsentStatusRawValue.allOptionsString()) + } + + let consentType = ConsentType(rawValue: String(describing: consentTypeRawValue)) + let consentStatus = ConsentStatus(rawValue: String(describing: consentStatusRawValue)) + + if firebaseConsentDict.keys.contains(consentType) { + throw OSFANLError.duplicateItemsIn(parameter: "ConsentSettings") + } else { + firebaseConsentDict[consentType] = consentStatus + } + } + + if firebaseConsentDict.isEmpty { + throw OSFANLError.missing("ConsentSettings") + } else { + return firebaseConsentDict + } + } +} diff --git a/src/ios/FirebaseAnalyticsPlugin.h b/src/ios/FirebaseAnalyticsPlugin.h index 9f89b02..da923f0 100644 --- a/src/ios/FirebaseAnalyticsPlugin.h +++ b/src/ios/FirebaseAnalyticsPlugin.h @@ -11,5 +11,6 @@ - (void)resetAnalyticsData:(CDVInvokedUrlCommand*)command; - (void)setDefaultEventParameters:(CDVInvokedUrlCommand*)command; - (void)requestTrackingAuthorization:(CDVInvokedUrlCommand*)command; +- (void)setConsent:(CDVInvokedUrlCommand*)command; @end diff --git a/src/ios/FirebaseAnalyticsPlugin.m b/src/ios/FirebaseAnalyticsPlugin.m index 96f2e6f..47c93f2 100644 --- a/src/ios/FirebaseAnalyticsPlugin.m +++ b/src/ios/FirebaseAnalyticsPlugin.m @@ -143,6 +143,18 @@ - (void)showTrackingAuthorizationPopup:(CDVInvokedUrlCommand *)command { [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; } +- (void)setConsent:(CDVInvokedUrlCommand*)command +{ + NSError *error; + NSDictionary *consentModel = [OSFANLConsentHelper createConsentModel:command.arguments error:&error]; + if (error) { + [self sendError:error forCallbackId:command.callbackId]; + } else { + [FIRAnalytics setConsent:consentModel]; + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK] callbackId:command.callbackId]; + } +} + typedef void (^showPermissionInformationPopupHandler)(UIAlertAction*); - (void)showPermissionInformationPopup: (NSString *)title : @@ -177,5 +189,4 @@ - (void)sendError:(NSError *)error forCallbackId:(NSString *)callbackId { [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; } - @end diff --git a/www/FirebaseAnalytics.js b/www/FirebaseAnalytics.js index d2d980f..bf2734f 100644 --- a/www/FirebaseAnalytics.js +++ b/www/FirebaseAnalytics.js @@ -55,5 +55,22 @@ module.exports = { logECommerceEvent: function(event, eventParameters, items, success, error) { let args = [{event, eventParameters, items}]; exec(success, error, PLUGIN_NAME, 'logECommerceEvent', args); + }, + /** + * setConsent + * + * @param {string} consentSettings - A JSON string of an object containing consent settings. + * @param {function} [success] - Success callback function. + * @param {function} [error] - Error callback function. + * + * @example + * const consentSettings = { + * AD_STORAGE: 'GRANTED', + * ANALYTICS_STORAGE: 'GRANTED', + * }; + * FirebaseAnalytics.setConsent(JSON.stringify(consentSettings)); + */ + setConsent: function (consentSettings, success, error) { + exec(success, error, PLUGIN_NAME, 'setConsent', [consentSettings]); } };