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]); } };