Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.ReadableType
import com.facebook.react.bridge.WritableMap
import com.facebook.react.modules.core.DeviceEventManagerModule
import com.mparticle.MParticle
Expand All @@ -23,6 +24,7 @@ import com.mparticle.rokt.RoktConfig
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import java.math.BigDecimal

class MPRoktModuleImpl(
private val reactContext: ReactApplicationContext,
Expand Down Expand Up @@ -121,9 +123,29 @@ class MPRoktModuleImpl(
reactContext?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)?.emit(eventName, params)
}

fun readableMapToMapOfStrings(attributes: ReadableMap?): Map<String, String> =
attributes?.toHashMap()?.filter { it.value is String }?.mapValues { it.value as String }
?: emptyMap()
fun readableMapToMapOfStrings(attributes: ReadableMap?): Map<String, String> {
if (attributes == null) {
return emptyMap()
}

val result = mutableMapOf<String, String>()
val iterator = attributes.keySetIterator()
while (iterator.hasNextKey()) {
val key = iterator.nextKey()
when (attributes.getType(key)) {
ReadableType.String -> attributes.getString(key)?.let { result[key] = it }
ReadableType.Number -> result[key] = formatNumberAttribute(attributes.getDouble(key))
ReadableType.Boolean ->
result[key] = if (attributes.getBoolean(key)) "true" else "false"
ReadableType.Map ->
attributes.getMap(key)?.toHashMap()?.let { result[key] = it.toString() }
ReadableType.Array ->
attributes.getArray(key)?.toArrayList()?.let { result[key] = it.toString() }
ReadableType.Null -> Unit
}
}
return result
}

fun String.toColorMode(): RoktConfig.ColorMode =
when (this) {
Expand Down Expand Up @@ -274,5 +296,17 @@ class MPRoktModuleImpl(
companion object {
const val MAX_LISTENERS = 5
const val MODULE_NAME = "RNMPRokt"

// Match iOS NSNumber stringValue: plain decimals, no trailing ".0", no scientific notation.
internal fun formatNumberAttribute(value: Double): String {
if (value.isNaN() || value.isInfinite()) {
return value.toString()
}
val longValue = value.toLong()
if (value == longValue.toDouble()) {
return longValue.toString()
}
return BigDecimal.valueOf(value).stripTrailingZeros().toPlainString()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.mparticle.react.rokt

import com.facebook.react.bridge.ReactApplicationContext
import com.mparticle.react.testutils.MockMap
import org.junit.Assert.assertEquals
import org.junit.Test
import org.mockito.Mockito.mock

class MPRoktModuleImplTest {
private val impl = MPRoktModuleImpl(mock(ReactApplicationContext::class.java))

@Test
fun readableMapToMapOfStrings_coercesNumbersAndBooleans() {
val attributes =
MockMap(
mapOf(
"amount" to 23.47,
"confirmationref" to 4600097641L,
"sandbox" to true,
"country" to "US",
),
)

val result = impl.readableMapToMapOfStrings(attributes)

assertEquals("23.47", result["amount"])
assertEquals("4600097641", result["confirmationref"])
assertEquals("true", result["sandbox"])
assertEquals("US", result["country"])
}

@Test
fun readableMapToMapOfStrings_skipsNullValues() {
val attributes = MockMap()
attributes.putString("country", "US")
attributes.putNull("amount")

val result = impl.readableMapToMapOfStrings(attributes)

assertEquals(mapOf("country" to "US"), result)
}

@Test
fun readableMapToMapOfStrings_formatsWholeNumberDoublesWithoutTrailingDecimal() {
val attributes = MockMap(mapOf("quantity" to 100.0))

val result = impl.readableMapToMapOfStrings(attributes)

assertEquals("100", result["quantity"])
}

@Test
fun formatNumberAttribute_avoidsScientificNotation() {
assertEquals("10000000000", MPRoktModuleImpl.formatNumberAttribute(1.0e10))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ public boolean isNull(String name) {

@Override
public boolean getBoolean(String name) {
return (boolean) map.get(name);
return (Boolean) map.get(name);
}

@Override
public double getDouble(String name) {
return (double) map.get(name);
return ((Number) map.get(name)).doubleValue();
}

@Override
Expand Down
37 changes: 29 additions & 8 deletions ios/RNMParticle/RNMPRokt.mm
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ - (void)ensureEventManager {
}
_rokt_log(@"[mParticle-Rokt] safeExtractRoktConfigDict: extracting config");
NSMutableDictionary *roktConfigDict = [[NSMutableDictionary alloc] init];
if (roktConfig.colorMode() != nil) {
roktConfigDict[@"colorMode"] = roktConfig.colorMode();
_rokt_log(@"[mParticle-Rokt] safeExtractRoktConfigDict: colorMode present");
}
if (roktConfig.cacheConfig().has_value()) {
NSMutableDictionary *cacheConfigDict = [[NSMutableDictionary alloc] init];
auto cacheConfig = roktConfig.cacheConfig().value();
Expand Down Expand Up @@ -274,19 +278,36 @@ - (void)getSessionIdWithResolve:(RCTPromiseResolveBlock)resolve
success:success];
}

- (NSMutableDictionary*)convertToMutableDictionaryOfStrings:(NSDictionary*)attributes
// Coerces React Native attribute values to strings, matching mParticle-Rokt kit
// transformValuesToString behavior (MPKitRokt.m).
- (NSMutableDictionary<NSString *, NSString *> *)convertToMutableDictionaryOfStrings:(NSDictionary *)attributes
{
NSMutableDictionary *finalAttributes = [attributes mutableCopy];
NSArray *keysForNullValues = [finalAttributes allKeysForObject:[NSNull null]];
[finalAttributes removeObjectsForKeys:keysForNullValues];
NSMutableDictionary<NSString *, NSString *> *finalAttributes = [[NSMutableDictionary alloc] init];
if (!attributes) {
return finalAttributes;
}

[attributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if (![key isKindOfClass:[NSString class]] || obj == nil || obj == [NSNull null]) {
return;
}

NSSet *keys = [finalAttributes keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) {
return ![obj isKindOfClass:[NSString class]];
NSString *stringKey = (NSString *)key;
if ([obj isKindOfClass:[NSString class]]) {
finalAttributes[stringKey] = obj;
} else if ([obj isKindOfClass:[NSNumber class]]) {
NSNumber *numberAttribute = (NSNumber *)obj;
if (numberAttribute == (id)kCFBooleanTrue || numberAttribute == (id)kCFBooleanFalse) {
finalAttributes[stringKey] = [numberAttribute boolValue] ? @"true" : @"false";
} else {
finalAttributes[stringKey] = [numberAttribute stringValue];
}
} else if ([obj isKindOfClass:[NSDictionary class]] || [obj isKindOfClass:[NSArray class]]) {
finalAttributes[stringKey] = [obj description];
}
}];

[finalAttributes removeObjectsForKeys:[keys allObjects]];
return finalAttributes;

}

- (RoktConfig *)buildRoktConfigFromDict:(NSDictionary<NSString *, id> *)configMap {
Expand Down
6 changes: 4 additions & 2 deletions js/codegenSpecs/rokt/NativeMPRokt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { TurboModuleRegistry } from 'react-native';

type ColorMode = string;

type RoktAttributeValue = string | number | boolean;

type CacheConfig = {
readonly cacheDurationInSeconds?: number;
readonly cacheAttributes?: { [key: string]: string };
Expand All @@ -16,7 +18,7 @@ type RoktConfigType = {
export interface Spec extends TurboModule {
selectPlacements(
identifier: string,
attributes?: { [key: string]: string },
attributes?: { [key: string]: RoktAttributeValue },
placeholders?: { [key: string]: number | null },
roktConfig?: RoktConfigType,
fontFilesMap?: { [key: string]: string }
Expand All @@ -30,7 +32,7 @@ export interface Spec extends TurboModule {

selectShoppableAds(
identifier: string,
attributes: { [key: string]: string },
attributes: { [key: string]: RoktAttributeValue },
roktConfig?: RoktConfigType
): void;

Expand Down
8 changes: 5 additions & 3 deletions js/rokt/rokt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@ import type { Spec as NativeMPRoktInterface } from '../codegenSpecs/rokt/NativeM

const MPRokt = getNativeModule<NativeMPRoktInterface>('RNMPRokt');

export type RoktAttributeValue = string | number | boolean;

export abstract class Rokt {
/**
* Selects placements with a [identifier], [attributes], optional [placeholders], optional [roktConfig], and optional [fontFilePathMap].
*
* @param {string} identifier - The page identifier for the placement.
* @param {Record<string, string>} attributes - Attributes to be associated with the placement.
* @param {Record<string, RoktAttributeValue>} attributes - Attributes to be associated with the placement.
* @param {Record<string, number | null>} [placeholders] - Optional placeholders for dynamic content.
* @param {IRoktConfig} [roktConfig] - Optional configuration settings for Rokt.
* @param {Record<string, string>} [fontFilesMap] - Optional mapping of font files.
* @returns {Promise<void>} A promise that resolves when the placement request is sent.
*/
static async selectPlacements(
identifier: string,
attributes: Record<string, string>,
attributes: Record<string, RoktAttributeValue>,
placeholders?: Record<string, number | null>,
roktConfig?: IRoktConfig,
fontFilesMap?: Record<string, string>
Expand All @@ -33,7 +35,7 @@ export abstract class Rokt {

static async selectShoppableAds(
identifier: string,
attributes: Record<string, string>,
attributes: Record<string, RoktAttributeValue>,
roktConfig?: IRoktConfig
): Promise<void> {
MPRokt.selectShoppableAds(identifier, attributes, roktConfig);
Expand Down
Loading