Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Expose device-level consent APIs: `setDeviceConsentState`, `clearDeviceConsentState`, and `getDeviceConsentState`, bridging to native `MParticle.deviceConsentState` (iOS) and `MParticle.setDeviceConsentState()` (Android). Requires mParticle Apple SDK 9.2+ with device consent; Android resolves `android-core` `[5.79.2, 6.0)` and picks up device consent APIs once published.

- Expo config plugin: optional `pinningDisabled` for `MPNetworkOptions` / `NetworkOptions` at SDK startup

### Fixed
Expand Down
81 changes: 81 additions & 0 deletions android/src/main/java/com/mparticle/react/MParticleModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,30 @@ class MParticleModule(
}
}

@ReactMethod
override fun setDeviceConsentState(consentState: ReadableMap?) {
val instance = MParticle.getInstance() ?: return
if (consentState == null) {
return
}
convertToConsentState(consentState)?.let { instance.setDeviceConsentState(it) }
}

@ReactMethod
override fun clearDeviceConsentState() {
MParticle.getInstance()?.setDeviceConsentState(null)
}

@ReactMethod
override fun getDeviceConsentState(callback: Callback) {
val instance = MParticle.getInstance()
if (instance == null) {
callback.invoke(null)
return
}
callback.invoke(consentStateToMap(instance.getDeviceConsentState()))
}

protected fun getWritableMap(): WritableMap = WritableNativeMap()

private fun convertIdentityAPIRequest(map: ReadableMap?): IdentityApiRequest {
Expand Down Expand Up @@ -973,4 +997,61 @@ class MParticleModule(
}
return builder.build()
}

private fun convertToConsentState(map: ReadableMap): ConsentState? {
Comment thread
samdozor marked this conversation as resolved.
Outdated
val builder = ConsentState.builder()
if (map.hasKey("gdpr")) {
map.getMap("gdpr")?.let { gdprMap ->
val iterator = gdprMap.keySetIterator()
while (iterator.hasNextKey()) {
val purpose = iterator.nextKey()
val consentMap = gdprMap.getMap(purpose) ?: continue
convertToGDPRConsent(consentMap)?.let { builder.addGDPRConsentState(purpose, it) }
}
}
}
if (map.hasKey("ccpa")) {
map.getMap("ccpa")?.let { ccpaMap ->
convertToCCPAConsent(ccpaMap)?.let { builder.setCCPAConsentState(it) }
}
}
return builder.build()
Comment thread
cursor[bot] marked this conversation as resolved.
}

private fun consentStateToMap(state: ConsentState?): WritableMap? {
if (state == null) {
return null
}
val result = Arguments.createMap()
val gdprConsentState = state.gdprConsentState
if (gdprConsentState.isNotEmpty()) {
val gdprMap = Arguments.createMap()
for ((purpose, consent) in gdprConsentState) {
gdprMap.putMap(purpose, gdprConsentToMap(consent))
}
result.putMap("gdpr", gdprMap)
}
state.ccpaConsentState?.let { result.putMap("ccpa", ccpaConsentToMap(it)) }
return if (result.toHashMap().isEmpty()) null else result
}

private fun gdprConsentToMap(consent: GDPRConsent): WritableMap {
val map = Arguments.createMap()
map.putBoolean("consented", consent.isConsented)
consent.document?.let { map.putString("document", it) }
consent.location?.let { map.putString("location", it) }
consent.hardwareId?.let { map.putString("hardwareId", it) }
consent.timestamp?.let { map.putDouble("timestamp", it.toDouble()) }
Comment thread
cursor[bot] marked this conversation as resolved.
return map
}

private fun ccpaConsentToMap(consent: CCPAConsent): WritableMap {
val map = Arguments.createMap()
map.putBoolean("consented", consent.isConsented)
consent.document?.let { map.putString("document", it) }
consent.location?.let { map.putString("location", it) }
consent.hardwareId?.let { map.putString("hardwareId", it) }
consent.timestamp?.let { map.putDouble("timestamp", it.toDouble()) }
return map
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ abstract class NativeMParticleSpec(

abstract fun removeCCPAConsentState()

abstract fun setDeviceConsentState(consentState: ReadableMap?)

abstract fun clearDeviceConsentState()

abstract fun getDeviceConsentState(callback: Callback)

abstract fun isKitActive(
kitId: Double,
callback: Callback,
Expand Down
151 changes: 149 additions & 2 deletions ios/RNMParticle/RNMParticle.mm
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@ @interface MParticleUser ()
- (void)setUserId:(NSNumber *)userId;
@end

@interface RNMParticle (DeviceConsent)
+ (NSDictionary *)consentStateToDictionary:(MPConsentState *)consentState;
@end

// Forward declare so New Arch `logCommerceEvent` can use the same JS→native
// mappings as `RCTConvert (MPCommerceEvent)` (defined later in this file).
@interface RCTConvert (MPCommerceEvent)
+ (MPCommerceEventAction)MPCommerceEventAction:(id)json;
+ (MPPromotionAction)MPPromotionAction:(id)json;
+ (MPConsentState *)MPConsentState:(id)json;
@end

@implementation RNMParticle
Expand Down Expand Up @@ -584,6 +589,33 @@ - (void)setCCPAConsentState:(JS::NativeMParticle::CCPAConsent &)consent {
[consentState setCCPAConsentState:ccpaConsent];
user.consentState = consentState;
}

- (void)setDeviceConsentState:(JS::NativeMParticle::DeviceConsentState &)consentState {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
id gdpr = consentState.gdpr();
if (gdpr != nil && gdpr != [NSNull null]) {
dict[@"gdpr"] = gdpr;
}
id ccpa = consentState.ccpa();
if (ccpa != nil && ccpa != [NSNull null]) {
dict[@"ccpa"] = ccpa;
}
[MParticle sharedInstance].deviceConsentState = [RCTConvert MPConsentState:dict];
}

- (void)clearDeviceConsentState {
[MParticle sharedInstance].deviceConsentState = nil;
}

- (void)getDeviceConsentState:(RCTResponseSenderBlock)callback {
MPConsentState *deviceConsent = [MParticle sharedInstance].deviceConsentState;
if (deviceConsent == nil) {
callback(@[[NSNull null]]);
return;
}
NSDictionary *consentDict = [RNMParticle consentStateToDictionary:deviceConsent];
callback(@[consentDict ?: [NSNull null]]);
}
#else

RCT_EXPORT_METHOD(logMPEvent:(MPEvent *)event)
Expand Down Expand Up @@ -614,6 +646,30 @@ - (void)setCCPAConsentState:(JS::NativeMParticle::CCPAConsent &)consent {
user.consentState = consentState;
}

RCT_EXPORT_METHOD(setDeviceConsentState:(NSDictionary *)consentState)
{
if (consentState == nil || consentState == (id)[NSNull null]) {
return;
}
[MParticle sharedInstance].deviceConsentState = [RCTConvert MPConsentState:consentState];
}

RCT_EXPORT_METHOD(clearDeviceConsentState)
{
[MParticle sharedInstance].deviceConsentState = nil;
}

RCT_EXPORT_METHOD(getDeviceConsentState:(RCTResponseSenderBlock)callback)
{
MPConsentState *deviceConsent = [MParticle sharedInstance].deviceConsentState;
if (deviceConsent == nil) {
callback(@[[NSNull null]]);
return;
}
NSDictionary *consentDict = [RNMParticle consentStateToDictionary:deviceConsent];
callback(@[consentDict ?: [NSNull null]]);
}

#endif

// Helper method to create MPProduct from dictionary
Expand Down Expand Up @@ -741,6 +797,59 @@ + (BOOL)isNumericIdentityKey:(NSString *)key {
return [numericSet isSupersetOfSet:keyCharacterSet];
}

+ (NSDictionary *)consentStateToDictionary:(MPConsentState *)consentState
{
if (consentState == nil) {
return nil;
}

NSMutableDictionary *result = [NSMutableDictionary dictionary];
NSDictionary<NSString *, MPGDPRConsent *> *gdprState = consentState.gdprConsentState;
if (gdprState.count > 0) {
NSMutableDictionary *gdpr = [NSMutableDictionary dictionaryWithCapacity:gdprState.count];
for (NSString *purpose in gdprState) {
MPGDPRConsent *consent = gdprState[purpose];
NSMutableDictionary *consentDict = [NSMutableDictionary dictionary];
consentDict[@"consented"] = @(consent.consented);
if (consent.document) {
consentDict[@"document"] = consent.document;
}
if (consent.timestamp) {
consentDict[@"timestamp"] = @((long long)([consent.timestamp timeIntervalSince1970] * 1000));
}
if (consent.location) {
consentDict[@"location"] = consent.location;
}
if (consent.hardwareId) {
consentDict[@"hardwareId"] = consent.hardwareId;
}
gdpr[purpose] = consentDict;
}
result[@"gdpr"] = gdpr;
}

MPCCPAConsent *ccpa = consentState.ccpaConsentState;
if (ccpa != nil) {
NSMutableDictionary *ccpaDict = [NSMutableDictionary dictionary];
ccpaDict[@"consented"] = @(ccpa.consented);
if (ccpa.document) {
ccpaDict[@"document"] = ccpa.document;
}
if (ccpa.timestamp) {
ccpaDict[@"timestamp"] = @((long long)([ccpa.timestamp timeIntervalSince1970] * 1000));
}
if (ccpa.location) {
ccpaDict[@"location"] = ccpa.location;
}
if (ccpa.hardwareId) {
ccpaDict[@"hardwareId"] = ccpa.hardwareId;
}
result[@"ccpa"] = ccpaDict;
}

return result.count > 0 ? result : nil;
}

@end

// RCTConvert category methods for mParticle types
Expand Down Expand Up @@ -938,6 +1047,7 @@ + (MParticleUser *)MParticleUser:(id)json;
+ (MPEvent *)MPEvent:(id)json;
+ (MPGDPRConsent *)MPGDPRConsent:(id)json;
+ (MPCCPAConsent *)MPCCPAConsent:(id)json;
+ (MPConsentState *)MPConsentState:(id)json;

@end

Expand Down Expand Up @@ -1206,7 +1316,9 @@ + (MPGDPRConsent *)MPGDPRConsent:(id)json {

mpConsent.consented = [RCTConvert BOOL:json[@"consented"]];
mpConsent.document = json[@"document"];
mpConsent.timestamp = [RCTConvert NSDate:json[@"timestamp"]];
if (json[@"timestamp"] && json[@"timestamp"] != [NSNull null]) {
mpConsent.timestamp = [NSDate dateWithTimeIntervalSince1970:[json[@"timestamp"] doubleValue] / 1000.0];
}
mpConsent.location = json[@"location"];
mpConsent.hardwareId = json[@"hardwareId"];

Expand All @@ -1218,11 +1330,46 @@ + (MPCCPAConsent *)MPCCPAConsent:(id)json {

mpConsent.consented = [RCTConvert BOOL:json[@"consented"]];
mpConsent.document = json[@"document"];
mpConsent.timestamp = [RCTConvert NSDate:json[@"timestamp"]];
if (json[@"timestamp"] && json[@"timestamp"] != [NSNull null]) {
mpConsent.timestamp = [NSDate dateWithTimeIntervalSince1970:[json[@"timestamp"] doubleValue] / 1000.0];
}
mpConsent.location = json[@"location"];
mpConsent.hardwareId = json[@"hardwareId"];

return mpConsent;
}

+ (MPConsentState *)MPConsentState:(id)json
Comment thread
cursor[bot] marked this conversation as resolved.
{
if (![json isKindOfClass:[NSDictionary class]]) {
return nil;
}

NSDictionary *dict = (NSDictionary *)json;
MPConsentState *state = [[MPConsentState alloc] init];
NSDictionary *gdpr = dict[@"gdpr"];
if ([gdpr isKindOfClass:[NSDictionary class]]) {
for (NSString *purpose in gdpr) {
id consentJson = gdpr[purpose];
if (consentJson == [NSNull null]) {
continue;
}
MPGDPRConsent *consent = [RCTConvert MPGDPRConsent:consentJson];
Comment thread
BrandonStalnaker marked this conversation as resolved.
if (consent != nil) {
[state addGDPRConsentState:consent purpose:purpose];
}
}
}

id ccpaJson = dict[@"ccpa"];
if (ccpaJson != nil && ccpaJson != [NSNull null]) {
MPCCPAConsent *ccpa = [RCTConvert MPCCPAConsent:ccpaJson];
if (ccpa != nil) {
[state setCCPAConsentState:ccpa];
}
}

return state;
}

@end
10 changes: 10 additions & 0 deletions js/codegenSpecs/NativeMParticle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ export interface CCPAConsent {
hardwareId?: string | null;
}

export interface DeviceConsentState {
gdpr?: { [purpose: string]: GDPRConsent };
ccpa?: CCPAConsent | null;
}

export type AttributionResult = {
[key: string]: {
[key: string]: string | number | boolean;
Expand Down Expand Up @@ -139,6 +144,11 @@ export interface Spec extends TurboModule {
removeGDPRConsentStateWithPurpose(purpose: string): void;
setCCPAConsentState(consent: CCPAConsent): void;
removeCCPAConsentState(): void;
setDeviceConsentState(consentState: DeviceConsentState): void;
clearDeviceConsentState(): void;
getDeviceConsentState(
callback: (result: DeviceConsentState | null) => void
): void;
isKitActive(kitId: number, callback: (result: boolean) => void): void;
getAttributions(callback: (result: AttributionResult) => void): void;
logPushRegistration(token: string, senderId: string): void;
Expand Down
22 changes: 22 additions & 0 deletions js/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
Spec as NativeMParticleInterface,
CallbackError,
UserAttributes as NativeUserAttributes,
DeviceConsentState,
} from './codegenSpecs/NativeMParticle';
import { getNativeModule } from './utils/architecture';

Expand Down Expand Up @@ -210,6 +211,24 @@ export const removeCCPAConsentState = (): void => {
MParticleModule.removeCCPAConsentState();
};

export type { DeviceConsentState };

export const setDeviceConsentState = (
consentState: DeviceConsentState
): void => {
MParticleModule.setDeviceConsentState(consentState);
};

export const clearDeviceConsentState = (): void => {
MParticleModule.clearDeviceConsentState();
};

export const getDeviceConsentState = (
completion: CompletionCallback<DeviceConsentState | null>
): void => {
MParticleModule.getDeviceConsentState(completion);
};

export const isKitActive = (
kitId: number,
completion: CompletionCallback<boolean>
Expand Down Expand Up @@ -930,6 +949,9 @@ const MParticle = {
removeGDPRConsentStateWithPurpose,
setCCPAConsentState,
removeCCPAConsentState,
setDeviceConsentState,
clearDeviceConsentState,
getDeviceConsentState,
isKitActive,
getAttributions,
logPushRegistration,
Expand Down
Loading