diff --git a/CHANGELOG.md b/CHANGELOG.md index 549677d2..296e6a55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ * `enterContentZone`, to start Content checks (Experimental!) * `exitContentZone`, to stop Content checks (Experimental!) * Added `Countly.deviceId.setID` method for changing device ID based on the device ID type +* Deprecated `Countly.setUserData` and replaced with `Countly.userProfile.setProperties` +* Deprecated `Countly.userData` and replaced with `Countly.userProfile`, also following calls are deprecated and replaced: + * `Countly.userData.setProperty` (replaced with: `Countly.userProfile.setProperty`) + * `Countly.userData.increment` (replaced with: `Countly.userProfile.increment`) + * `Countly.userData.incrementBy` (replaced with: `Countly.userProfile.incrementBy`) + * `Countly.userData.multiply` (replaced with: `Countly.userProfile.multiply`) + * `Countly.userData.saveMax` (replaced with: `Countly.userProfile.saveMax`) + * `Countly.userData.saveMin` (replaced with: `Countly.userProfile.saveMin`) + * `Countly.userData.setOnce` (replaced with: `Countly.userProfile.setOnce`) + * `Countly.userData.pushUniqueValue` (replaced with: `Countly.userProfile.pushUnique`) + * `Countly.userData.pushValue` (replaced with: `Countly.userProfile.push`) + * `Countly.userData.pullValue` (replaced with: `Countly.userProfile.pull`) * Mitigated an issue where a session could have started while the app was in the background when the device ID was changed (non-merge). * Deprecated following SDK calls: * `Countly.getCurrentDeviceId` (replaced with: `Countly.deviceId.getID`) diff --git a/Countly.d.ts b/Countly.d.ts index de3ad085..ac1613a9 100644 --- a/Countly.d.ts +++ b/Countly.d.ts @@ -572,6 +572,7 @@ declare module "countly-sdk-react-native-bridge" { /** * + * @deprecated 'setUserData' is deprecated use 'Countly.userProfile.setProperties' instead. * Used to send user data * * @param {object} userData user data @@ -579,9 +580,122 @@ declare module "countly-sdk-react-native-bridge" { */ export function setUserData(userData: CountlyUserData): string | Promise; + namespace userProfile { + /** + * + * Set custom key and value pair for the current user + * + * @param {string} keyName - user property key + * @param {object} keyValue - user property value + * @returns {void} + */ + export function setProperty(keyName: string, keyValue: any): Promise; + + /** + * + * Set predefined and/or custom key and value pairs for the current user + * + * @param {object} userData - custom key value pairs + * @returns {void} + */ + export function setProperties(userData: CountlyUserData): Promise; + + /** + * + * Increment custom user data by 1 + * + * @param {string} keyName - user property key + * @returns {void} + */ + export function increment(keyName: string): Promise; + + /** + * + * Increment custom user data by a specified value + * + * @param {string} keyName - user property key + * @param {number} keyValue - value to increment user property by + * @returns {void} + */ + export function incrementBy(keyName: string, keyValue: any): Promise; + + /** + * + * Multiply custom user data by a specified value + * + * @param {string} keyName - user property key + * @param {number} keyValue - value to multiply user property by + * @returns {void} + */ + export function multiply(keyName: string, keyValue: any): Promise; + + /** + * + * Save the max value between current and provided value + * + * @param {string} keyName - user property key + * @param {number} keyValue - user property value + * @returns {void} + */ + export function saveMax(keyName: string, keyValue: any): Promise; + + /** + * + * Save the min value between current and provided value + * + * @param {string} keyName - user property key + * @param {number} keyValue - user property value + * @returns {void} + */ + export function saveMin(keyName: string, keyValue: any): Promise; + + /** + * + * Set the property value if it does not exist + * + * @param {string} keyName - user property key + * @param {string} keyValue - user property value + * @returns {void} + */ + export function setOnce(keyName: string, keyValue: any): Promise; + + /** + * + * Add value to custom property (array) if value does not exist within + * + * @param {string} keyName user property key + * @param {string} keyValue user property value + * @returns {void} + */ + export function pushUnique(keyName: string, keyValue: any): Promise; + + /** + * + * Add value to custom property (array) + * + * @param {string} keyName user property key + * @param {string} keyValue user property value + * @returns {void} + */ + export function push(keyName: string, keyValue: any): Promise; + + /** + * + * Remove value from custom property (array) + * + * @param {string} keyName user property key + * @param {string} keyValue user property value + * @returns {void} + */ + export function pull(keyName: string, keyValue: any): Promise; + } + + /** + * @deprecated Countly.userData is deprecated, use Countly.userProfile instead + */ namespace userData { /** - * + * @deprecated 'Countly.userData.setProperty' is deprecated, use 'Countly.userProfile.setProperty' instead. * Set custom key and value pair for the current user. * * @param {string} keyName user property key @@ -591,7 +705,7 @@ declare module "countly-sdk-react-native-bridge" { export function setProperty(keyName: string, keyValue: any): Promise | string; /** - * + * @deprecated 'Countly.userData.increment' is deprecated, use 'Countly.userProfile.increment' instead. * Increment custom user data by 1 * * @param {string} keyName user property key @@ -600,7 +714,7 @@ declare module "countly-sdk-react-native-bridge" { export function increment(keyName: string): Promise | string; /** - * + * @deprecated 'Countly.userData.incrementBy' is deprecated, use 'Countly.userProfile.incrementBy' instead. * Increment custom user data by a specified value * * @param {string} keyName user property key @@ -610,7 +724,7 @@ declare module "countly-sdk-react-native-bridge" { export function incrementBy(keyName: string, keyValue: any): Promise | string; /** - * + * @deprecated 'Countly.userData.multiply' is deprecated, use 'Countly.userProfile.multiply' instead. * Multiply custom user data by a specified value * * @param {string} keyName user property key @@ -620,7 +734,7 @@ declare module "countly-sdk-react-native-bridge" { export function multiply(keyName: string, keyValue: any): Promise | string; /** - * + * @deprecated 'Countly.userData.saveMax' is deprecated, use 'Countly.userProfile.saveMax' instead. * Save the max value between current and provided value. * * @param {string} keyName user property key @@ -630,7 +744,7 @@ declare module "countly-sdk-react-native-bridge" { export function saveMax(keyName: string, keyValue: any): Promise | string; /** - * + * @deprecated 'Countly.userData.saveMin' is deprecated, use 'Countly.userProfile.saveMin' instead. * Save the min value between current and provided value. * * @param {string} keyName user property key @@ -640,7 +754,7 @@ declare module "countly-sdk-react-native-bridge" { export function saveMin(keyName: string, keyValue: any): Promise | string; /** - * + * @deprecated 'Countly.userData.setOnce' is deprecated, use 'Countly.userProfile.setOnce' instead. * Set the property value if it does not exist. * * @param {string} keyName user property key @@ -650,7 +764,7 @@ declare module "countly-sdk-react-native-bridge" { export function setOnce(keyName: string, keyValue: any): Promise | string; /** - * + * @deprecated 'Countly.userData.pushUniqueValue' is deprecated, use 'Countly.userProfile.pushUnique' instead. * Add value to custom property (array) if value does not exist within. * * @param {string} keyName user property key @@ -660,7 +774,7 @@ declare module "countly-sdk-react-native-bridge" { export function pushUniqueValue(keyName: string, keyValue: any): Promise | string; /** - * + * @deprecated 'Countly.userData.pushValue' is deprecated, use 'Countly.userProfile.push' instead. * Add value to custom property (array). * * @param {string} keyName user property key @@ -670,7 +784,7 @@ declare module "countly-sdk-react-native-bridge" { export function pushValue(keyName: string, keyValue: any): Promise | string; /** - * + * @deprecated 'Countly.userData.pullValue' is deprecated, use 'Countly.userProfile.pull' instead. * Remove value to custom property (array). * * @param {string} keyName user property key diff --git a/Countly.js b/Countly.js index a3072885..9e05293c 100644 --- a/Countly.js +++ b/Countly.js @@ -11,6 +11,7 @@ import CountlyState from "./CountlyState.js"; import Feedback from "./Feedback.js"; import Event from "./Event.js"; import DeviceId from "./DeviceId.js"; +import UserProfile from "./UserProfile.js"; import * as L from "./Logger.js"; import * as Utils from "./Utils.js"; import * as Validate from "./Validators.js"; @@ -28,9 +29,13 @@ CountlyState.eventEmitter = eventEmitter; Countly.feedback = new Feedback(CountlyState); Countly.events = new Event(CountlyState); Countly.deviceId = new DeviceId(CountlyState); +Countly.userProfile = new UserProfile(CountlyState); let _isCrashReportingEnabled = false; +/** + * @deprecated Countly.userData is deprecated, use Countly.userProfile instead + */ Countly.userData = {}; // userData interface Countly.userDataBulk = {}; // userDataBulk interface @@ -835,7 +840,7 @@ Countly.setUserData = async function (userData) { }; /** - * + * @deprecated 'Countly.userData.setProperty' is deprecated, use 'Countly.userProfile.setProperty' instead. * Set custom key and value pair for the current user. * * @param {string} keyName user property key @@ -866,7 +871,7 @@ Countly.userData.setProperty = async function (keyName, keyValue) { }; /** - * + * @deprecated 'Countly.userData.increment' is deprecated, use 'Countly.userProfile.increment' instead. * Increment custom user data by 1 * * @param {string} keyName user property key @@ -890,7 +895,7 @@ Countly.userData.increment = async function (keyName) { }; /** - * + * @deprecated 'Countly.userData.incrementBy' is deprecated, use 'Countly.userProfile.incrementBy' instead. * Increment custom user data by a specified value * * @param {string} keyName user property key @@ -917,7 +922,7 @@ Countly.userData.incrementBy = async function (keyName, keyValue) { }; /** - * + * @deprecated 'Countly.userData.multiply' is deprecated, use 'Countly.userProfile.multiply' instead. * Multiply custom user data by a specified value * * @param {string} keyName user property key @@ -944,7 +949,7 @@ Countly.userData.multiply = async function (keyName, keyValue) { }; /** - * + * @deprecated 'Countly.userData.saveMax' is deprecated, use 'Countly.userProfile.saveMax' instead. * Save the max value between current and provided value. * * @param {string} keyName user property key @@ -971,7 +976,7 @@ Countly.userData.saveMax = async function (keyName, keyValue) { }; /** - * + * @deprecated 'Countly.userData.saveMin' is deprecated, use 'Countly.userProfile.saveMin' instead. * Save the min value between current and provided value. * * @param {string} keyName user property key @@ -998,7 +1003,7 @@ Countly.userData.saveMin = async function (keyName, keyValue) { }; /** - * + * @deprecated 'Countly.userData.setOnce' is deprecated, use 'Countly.userProfile.setOnce' instead. * Set the property value if it does not exist. * * @param {string} keyName user property key @@ -1027,7 +1032,7 @@ Countly.userData.setOnce = async function (keyName, keyValue) { }; /** - * + * @deprecated 'Countly.userData.pushUniqueValue' is deprecated, use 'Countly.userProfile.pushUnique' instead. * Add value to custom property (array) if value does not exist within. * * @param {string} keyName user property key @@ -1056,7 +1061,7 @@ Countly.userData.pushUniqueValue = async function (keyName, keyValue) { }; /** - * + * @deprecated 'Countly.userData.pushValue' is deprecated, use 'Countly.userProfile.push' instead. * Add value to custom property (array). * * @param {string} keyName user property key @@ -1085,7 +1090,7 @@ Countly.userData.pushValue = async function (keyName, keyValue) { }; /** - * + * @deprecated 'Countly.userData.pullValue' is deprecated, use 'Countly.userProfile.pull' instead. * Remove value to custom property (array). * * @param {string} keyName user property key diff --git a/UserProfile.js b/UserProfile.js new file mode 100644 index 00000000..393eeea6 --- /dev/null +++ b/UserProfile.js @@ -0,0 +1,262 @@ +import * as L from "./Logger.js"; + +class UserProfile { + #state; + + constructor(state) { + this.#state = state; + } + + static predefinedKeys = { + name: "string", + username: "string", + email: "string", + organization: "string", + phone: "string", + picture: "string", + picturePath: "string", + gender: "string", + byear: "number", + }; + + isValidUserProfileCall = function (keyName, keyValue, functionName) { + if (!this.#state.isInitialized) { + L.w(`${functionName}, 'init' must be called before ${functionName}`); + return false; + } + // validate keyName + if(!keyName) { + L.w(`${functionName}, provided keyName is null`); + return false; + } + if (typeof keyName !== 'string' || keyName.trim() === '') { + L.w(`${functionName}, provided keyName is not a valid string`); + return false; + } + // validate keyValue + if (keyValue === null) { + L.w(`${functionName}, provided keyValue is not valid`); + return false; + } + return true; + } + + /** + * + * Set custom key and value pair for the current user + * + * @param {string} keyName - user property key + * @param {object} keyValue - user property value + * @returns {void} + */ + setProperty = async function (keyName, keyValue) { + if(!this.isValidUserProfileCall(keyName, keyValue, "setProperty")) { + return; + } + let formattedKeyValue = keyValue.toString(); + L.d(`setProperty, Setting user property: [${keyName}, ${formattedKeyValue}]`); + await this.#state.CountlyReactNative.userData_setProperty([keyName, formattedKeyValue]); + }; + + /** + * + * Set predefined and/or custom key and value pairs for the current user + * + * @param {object} userData - custom key value pairs + * @returns {void} + */ + setProperties = async function (userData) { + if (!this.#state.isInitialized) { + L.w("setProperties, 'init' must be called before 'setProperties'"); + return; + } + if (!userData) { + L.w("setProperties, User profile data should not be null or undefined"); + return; + } + if (typeof userData !== "object") { + L.w(`setProperties, unsupported data type of user data '${typeof userData}'`); + return; + } + L.d(`setProperties, Setting properties: [${JSON.stringify(userData)}]`); + const userProfile = {}; + for (const key in userData) { + const value = userData[key]; + let expectedType = null; + + // To check if the key exists in predefinedKeys + if (Object.hasOwn(UserProfile.predefinedKeys, key)) { + expectedType = UserProfile.predefinedKeys[key]; + } + + if (expectedType) { + if (typeof value === expectedType || (key === "byear" && typeof value === "number")) { + userProfile[key] = key === "byear" ? value.toString() : value; + } else { + L.w(`setProperties, skipping key '${key}' due to type mismatch (expected: ${expectedType}, got: ${typeof value})`); + } + } else { + userProfile[key] = value; + } + } + await this.#state.CountlyReactNative.setProperties([userProfile]); + }; + + /** + * + * Increment custom user data by 1 + * + * @param {string} keyName - user property key + * @returns {void} + */ + increment = async function (keyName) { + if (!this.#state.isInitialized) { + L.w("increment, 'init' must be called before 'increment'"); + return; + } + if (!keyName || typeof keyName !== 'string') { + L.w("increment, provided keyName is not a valid string"); + return; + } + L.d(`increment, Incrementing user property: [${keyName}]`); + await this.#state.CountlyReactNative.userData_increment([keyName]); + }; + + /** + * + * Increment custom user data by a specified value + * + * @param {string} keyName - user property key + * @param {number} keyValue - value to increment user property by + * @returns {void} + */ + incrementBy = async function (keyName, keyValue) { + if(!this.isValidUserProfileCall(keyName, keyValue, "incrementBy")) { + return; + } + L.d(`incrementBy, Incrementing user property: [${keyName}, ${keyValue}]`); + const intValue = parseInt(keyValue, 10).toString(); + await this.#state.CountlyReactNative.userData_incrementBy([keyName, intValue]); + }; + + /** + * + * Multiply custom user data by a specified value + * + * @param {string} keyName - user property key + * @param {number} keyValue - value to multiply user property by + * @returns {void} + */ + multiply = async function (keyName, keyValue) { + if(!this.isValidUserProfileCall(keyName, keyValue, "multiply")) { + return; + } + L.d(`multiply, Multiplying user property: [${keyName}, ${keyValue}]`); + const intValue = parseInt(keyValue, 10).toString(); + await this.#state.CountlyReactNative.userData_multiply([keyName, intValue]); + }; + + /** + * + * Save the max value between current and provided value + * + * @param {string} keyName - user property key + * @param {number} keyValue - user property value + * @returns {void} + */ + saveMax = async function (keyName, keyValue) { + if(!this.isValidUserProfileCall(keyName, keyValue, "saveMax")) { + return; + } + L.d(`saveMax, Saving max user property: [${keyName}, ${keyValue}]`); + const intValue = parseInt(keyValue, 10).toString(); + await this.#state.CountlyReactNative.userData_saveMax([keyName, intValue]); + }; + + /** + * + * Save the min value between current and provided value + * + * @param {string} keyName - user property key + * @param {number} keyValue - user property value + * @returns {void} + */ + saveMin = async function (keyName, keyValue) { + if(!this.isValidUserProfileCall(keyName, keyValue, "saveMin")) { + return; + } + L.d(`saveMin, Saving min user property: [${keyName}, ${keyValue}]`); + const intValue = parseInt(keyValue, 10).toString(); + await this.#state.CountlyReactNative.userData_saveMin([keyName, intValue]); + }; + + /** + * + * Set the property value if it does not exist + * + * @param {string} keyName - The user property key. + * @param {boolean | number | string} keyValue - The user property value. + * @returns {void} + */ + setOnce = async function (keyName, keyValue) { + if(!this.isValidUserProfileCall(keyName, keyValue, "setOnce")) { + return; + } + keyValue = keyValue.toString(); + L.d(`setOnce, Setting once user property: [${keyName}, ${keyValue}]`); + await this.#state.CountlyReactNative.userData_setOnce([keyName, keyValue]); + }; + + /** + * + * Add value to custom property (array) if value does not exist within + * + * @param {string} keyName - The user property key. + * @param {boolean | number | string} keyValue - The user property value. + * @returns {void} + */ + pushUnique = async function (keyName, keyValue) { + if(!this.isValidUserProfileCall(keyName, keyValue, "pushUnique")) { + return; + } + keyValue = keyValue.toString(); + L.d(`pushUnique, Pushing unique value to user property: [${keyName}, ${keyValue}]`); + await this.#state.CountlyReactNative.userData_pushUniqueValue([keyName, keyValue]); + }; + + /** + * + * Add a value to a custom property (array). + * + * @param {string} keyName - The user property key. + * @param {boolean | number | string} keyValue - The user property value. + * @returns {void} + */ + push = async function (keyName, keyValue) { + if(!this.isValidUserProfileCall(keyName, keyValue, "push")) { + return; + } + keyValue = keyValue.toString(); + L.d(`push, Pushing value to user property: [${keyName}, ${keyValue}]`); + await this.#state.CountlyReactNative.userData_pushValue([keyName, keyValue]); + }; + + /** + * + * Remove value from custom property (array) + * + * @param {string} keyName - The user property key. + * @param {boolean | number | string} keyValue - The user property value. + * @returns {void} + */ + pull = async function (keyName, keyValue) { + if(!this.isValidUserProfileCall(keyName, keyValue, "push")) { + return; + } + keyValue = keyValue.toString(); + L.d(`push, Pulling value from user property: [${keyName}, ${keyValue}]`); + await this.#state.CountlyReactNative.userData_pullValue([keyName, keyValue]); + }; +} + +export default UserProfile; \ No newline at end of file diff --git a/android/src/main/java/ly/count/android/sdk/react/CountlyReactNative.java b/android/src/main/java/ly/count/android/sdk/react/CountlyReactNative.java index d3207f0d..1c2db3a8 100644 --- a/android/src/main/java/ly/count/android/sdk/react/CountlyReactNative.java +++ b/android/src/main/java/ly/count/android/sdk/react/CountlyReactNative.java @@ -793,6 +793,21 @@ public void setUserData(ReadableArray args, Promise promise) { promise.resolve("Success"); } + @ReactMethod + public void setProperties(ReadableArray args, Promise promise) { + Countly.sharedInstance(); + ReadableMap userData = args.getMap(0); + Map userDataObjectMap = userData.toHashMap(); + + for (Map.Entry entry : userDataObjectMap.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + userDataObjectMap.put(key, value); + } + Countly.sharedInstance().userProfile().setProperties(userDataObjectMap); + promise.resolve("Success"); + } + @ReactMethod public void sendPushToken(ReadableArray args) { String pushToken = args.getString(0); diff --git a/example/CountlyRNExample/UserProfiles.tsx b/example/CountlyRNExample/UserProfiles.tsx index 55cec93e..4bd9a8d0 100644 --- a/example/CountlyRNExample/UserProfiles.tsx +++ b/example/CountlyRNExample/UserProfiles.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from 'react'; import { ScrollView } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import Countly from "countly-sdk-react-native-bridge"; @@ -135,24 +135,102 @@ const userData_pullValue = () => { Countly.userData.pullValue("type", "morning"); }; +const onSetProperty = () => { + Countly.userProfile.setProperty("Griselda", ["Conway", "Benny", "Westside Gunn"]); +}; + +const onSetProperties = () => { + const userData = { + name: "John Doe", // predefined key, valid type (string) + username: "johndoe123", // predefined key, valid type (string) + email: "john.doe@example.com", // predefined key, valid type (string) + organization: "Example Corp", // predefined key, valid type (string) + phone: "+1234567890", // predefined key, valid type (string) + picture: "https://example.com/avatar.jpg", // predefined key, valid type (string) + gender: "M", // predefined key, valid type (string) + byear: 1985, // predefined key, valid type (integer) + customKey1: "Custom Value 1", // custom key + customKey2: 42, // custom key + customKey3: ["item1", "item2"], // custom key (array) + }; + Countly.userProfile.setProperties(userData); +} + +const onIncrement = () => { + Countly.userProfile.increment("IncrementValue"); +} + +const onIncrementBy = () => { + Countly.userProfile.incrementBy("IncrementByValue", 2); +} + +const onMultiply = () => { + Countly.userProfile.multiply("MultiplyValue", 3); +} + +const onMax = () => { + Countly.userProfile.saveMax("Max", 4); +} + +const onMin = () => { + Countly.userProfile.saveMin("Min", 5); +} + +const onSetOnce = () => { + Countly.userProfile.setOnce("Once", 6); +} + +const onPushUnique = () => { + Countly.userProfile.pushUnique("Unique", 7); +} + +const onPush = () => { + Countly.userProfile.push("push", 8); +} + +const onPull = () => { + Countly.userProfile.pull("pull", 9); +} + function UserProfilesScreen({ navigation }) { - return ( + const [showDeprecated, setShowDeprecated] = useState(false); + + const toggleDeprecated = () => { + setShowDeprecated(prevState => !prevState); + }; + return ( - - - - - - - - - - - - - - + + + + + + + + + + + + + {showDeprecated && ( + + + + + + + + + + + + + + + + + )} ); diff --git a/ios/src/CountlyReactNative.m b/ios/src/CountlyReactNative.m index 923dd17d..e367db4d 100644 --- a/ios/src/CountlyReactNative.m +++ b/ios/src/CountlyReactNative.m @@ -311,10 +311,10 @@ - (void) populateConfig:(id) json { }); } -RCT_REMAP_METHOD(setUserData, params : (NSArray *)arguments setUserDataWithResolver : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) { +RCT_REMAP_METHOD(setProperties, params : (NSArray *)arguments setPropertiesWithResolver : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_main_queue(), ^{ NSDictionary *userData = [arguments objectAtIndex:0]; - [self setUserDataIntenral:userData]; + [self setPropertiesInternal:userData]; [Countly.user save]; resolve(@"Success"); }); @@ -776,7 +776,7 @@ - (CLLocationCoordinate2D)getCoordinate:(NSString *)gpsCoordinate { RCT_REMAP_METHOD(userDataBulk_setUserProperties, params : (NSDictionary *)userProperties userDataBulkSetUserPropertiesWithResolver : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_main_queue(), ^{ - [self setUserDataIntenral:userProperties]; + [self setPropertiesInternal:userProperties]; NSDictionary *customeProperties = [self removePredefinedUserProperties:userProperties]; Countly.user.custom = customeProperties; resolve(@"Success"); @@ -1346,7 +1346,7 @@ - (NSDictionary *)removePredefinedUserProperties:(NSDictionary *__nullable)userD return userProperties; } -- (void)setUserDataIntenral:(NSDictionary *__nullable)userData { +- (void)setPropertiesInternal:(NSDictionary *__nullable)userData { NSString *name = userData[NAME_KEY]; NSString *username = userData[USERNAME_KEY]; NSString *email = userData[EMAIL_KEY]; @@ -1380,6 +1380,20 @@ - (void)setUserDataIntenral:(NSDictionary *__nullable)userData { if (byear) { Countly.user.birthYear = @([byear integerValue]); } + + // Handle custom fields + NSMutableDictionary *customFields = [NSMutableDictionary dictionary]; + NSArray *predefinedKeys = @[NAME_KEY, USERNAME_KEY, EMAIL_KEY, ORG_KEY, PHONE_KEY, PICTURE_KEY, GENDER_KEY, BYEAR_KEY]; + + for (NSString *key in userData) { + if (![predefinedKeys containsObject:key]) { + customFields[key] = userData[key]; + } + } + + if (customFields.count > 0) { + Countly.user.custom = customFields; + } } @end