diff --git a/Singular-React-Native.podspec b/Singular-React-Native.podspec index 06244ff..f05b8f3 100644 --- a/Singular-React-Native.podspec +++ b/Singular-React-Native.podspec @@ -9,10 +9,34 @@ Pod::Spec.new do |spec| spec.homepage = "https://www.singular.net/" spec.author = "Singular Labs" spec.source = { :git => "https://github.com/singular-labs/react-native-sdk.git", :tag => spec.version.to_s } - spec.source_files = "ios/*.{h,m}" + # Source files based on architecture + if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then + spec.source_files = "ios/SingularBridge.h", "ios/SingularBridgeNewArch.mm", "ios/SingularHelper.h", "ios/SingularHelper.m" + else + spec.source_files = "ios/SingularBridge.h", "ios/SingularBridgeOldArch.m", "ios/SingularHelper.h", "ios/SingularHelper.m" + end spec.platform = :ios, "12.0" - spec.dependency 'Singular-SDK', '12.8.1' + spec.dependency 'Singular-SDK', '12.9.0' spec.static_framework = true spec.dependency 'React' + + # New Architecture support + if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then + spec.compiler_flags = "-DRCT_NEW_ARCH_ENABLED=1" + spec.pod_target_xcconfig = { + 'HEADER_SEARCH_PATHS' => '"$(PODS_ROOT)/boost" "${PODS_TARGET_SRCROOT}/../../ios/build/generated/ios"', + "CLANG_CXX_LANGUAGE_STANDARD" => "c++17", + "CLANG_CXX_LIBRARY" => "libc++", + "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -DFOLLY_CFG_NO_COROUTINES=1" + } + spec.dependency "React-Codegen" + spec.dependency "RCT-Folly" + spec.dependency "RCTRequired" + spec.dependency "RCTTypeSafety" + spec.dependency "ReactCommon/turbomodule/core" + else + spec.compiler_flags = "-DRCT_NEW_ARCH_ENABLED=0" + end end + diff --git a/Singular.js b/Singular.js index 2b365ba..49f14f8 100644 --- a/Singular.js +++ b/Singular.js @@ -1,88 +1,154 @@ -import {NativeEventEmitter, NativeModules, Platform} from 'react-native'; -import {version} from './package.json'; - -const {SingularBridge} = NativeModules; +import {NativeEventEmitter, NativeModules, Platform, TurboModuleRegistry} from 'react-native'; +import packageJson from './package.json'; const SDK_NAME = 'ReactNative'; -const SDK_VERSION = version; +const SDK_VERSION = packageJson.version; const ADMON_REVENUE_EVENT_NAME = '__ADMON_USER_LEVEL_REVENUE__'; -export class Singular { +let SingularBridge; +let isNewArch = false; + +const isNewArchitectureEnabled = global?.nativeFabricUIManager != null + +try { + if (isNewArchitectureEnabled) { + const turboModule = TurboModuleRegistry?.get?.('SingularBridge'); + if (turboModule) { + SingularBridge = turboModule; + isNewArch = true; + __DEV__ && console.log('[Singular SDK] Using New Architecture (TurboModule)'); + } else { + throw new Error('TurboModule not available'); + } + } else { + throw new Error('New Architecture not enabled'); + } +} catch (error) { + SingularBridge = NativeModules.SingularBridge; + __DEV__ && console.log('[Singular SDK] Using Old Architecture (Legacy Bridge)'); +} +export class Singular { static _singularNativeEmitter = new NativeEventEmitter(SingularBridge); static init(singularConfig) { + if (!singularConfig) { + __DEV__ && console.log('[Singular SDK] singularConfig must be a non-null object'); + return; + } + + const isPlainObject = + typeof singularConfig === 'object' && + (!singularConfig.constructor || singularConfig.constructor.name === 'Object'); + + const handlerProperties = [ + 'singularLinkHandler', + 'conversionValueUpdatedHandler', + 'conversionValuesUpdatedHandler', + 'deviceAttributionCallbackHandler', + 'didSetSdidCallback', + 'sdidReceivedCallback', + ]; + + if (singularConfig.singularLinkHandler && !this._singularLinkHandler) { this._singularLinkHandler = singularConfig.singularLinkHandler; + } + if (singularConfig.conversionValueUpdatedHandler && !this._conversionValueUpdatedHandler) { this._conversionValueUpdatedHandler = singularConfig.conversionValueUpdatedHandler; + } + if (singularConfig.conversionValuesUpdatedHandler && !this._conversionValuesUpdatedHandler) { this._conversionValuesUpdatedHandler = singularConfig.conversionValuesUpdatedHandler; + } + if (singularConfig.deviceAttributionCallbackHandler && !this._deviceAttributionCallbackHandler) { this._deviceAttributionCallbackHandler = singularConfig.deviceAttributionCallbackHandler; - + } + if (singularConfig.didSetSdidCallback && !this._didSetSdidCallback) { this._didSetSdidCallback = singularConfig.didSetSdidCallback; + } + if (singularConfig.sdidReceivedCallback && !this._sdidReceivedCallback) { this._sdidReceivedCallback = singularConfig.sdidReceivedCallback; + } + + // register callback listeners + this.registerListeners(); + + const configWithoutHandlers = Object.keys(singularConfig) + .filter( + (key) => + !handlerProperties.includes(key) && singularConfig[key] !== undefined + ) + .reduce((obj, key) => { + obj[key] = singularConfig[key]; + return obj; + }, {}); + + if (isNewArch) { + // If it's not a plain object (e.g., SingularConfig instance), serialize it + // Otherwise pass the plain object directly + var configForTurbo = null; + if (isPlainObject) { + configForTurbo = configWithoutHandlers; + } else { + configForTurbo = JSON.parse(JSON.stringify(configWithoutHandlers)); + } + + SingularBridge.init(configForTurbo); + return; + } + + SingularBridge.init(JSON.stringify(singularConfig)); + SingularBridge.setReactSDKVersion(SDK_NAME, SDK_VERSION); + } + + static registerListeners() { + if (this._singularLinkHandler) { + this._singularNativeEmitter.addListener('SingularLinkHandler', + (params) => { + this._singularLinkHandler?.(params); + }); + } + + if (this._deviceAttributionCallbackHandler) { + this._singularNativeEmitter.addListener('DeviceAttributionCallbackHandler', + (attributes) => this._deviceAttributionCallbackHandler?.(attributes)); + } + + if (this._conversionValueUpdatedHandler) { + this._singularNativeEmitter.addListener('ConversionValueUpdatedHandler', + (conversionValue) => this._conversionValueUpdatedHandler?.(conversionValue)); + } + + if (this._conversionValuesUpdatedHandler) { + this._singularNativeEmitter.addListener('ConversionValuesUpdatedHandler', + (updatedConversionValues) => this._conversionValuesUpdatedHandler?.(updatedConversionValues)); + } - this._singularNativeEmitter.addListener( - 'SingularLinkHandler', - singularLinkParams => { - if (this._singularLinkHandler) { - this._singularLinkHandler(singularLinkParams); - } - }); - - this._singularNativeEmitter.addListener( - 'ConversionValueUpdatedHandler', - conversionValue => { - if (this._conversionValueUpdatedHandler) { - this._conversionValueUpdatedHandler(conversionValue); - } - }); - - this._singularNativeEmitter.addListener( - 'ConversionValuesUpdatedHandler', - updatedConversionValues => { - if (this._conversionValuesUpdatedHandler) { - this._conversionValuesUpdatedHandler(updatedConversionValues); - } - }); - - this._singularNativeEmitter.addListener( - 'DeviceAttributionCallbackHandler', - attributes => { - if (this._deviceAttributionCallbackHandler) { - this._deviceAttributionCallbackHandler(attributes); - } - }); - - this._singularNativeEmitter.addListener( - 'SdidReceivedCallback', - result => { - if (this._sdidReceivedCallback) { - this._sdidReceivedCallback(result); - } - }); - - this._singularNativeEmitter.addListener( - 'DidSetSdidCallback', - result => { - if (this._didSetSdidCallback) { - this._didSetSdidCallback(result); - } - }); - - SingularBridge.init(JSON.stringify(singularConfig)); - SingularBridge.setReactSDKVersion(SDK_NAME, SDK_VERSION); - } - - static createReferrerShortLink(baseLink, referrerName, referrerId, passthroughParams, completionHandler){ - let eventSubscription = this._singularNativeEmitter.addListener( - 'ShortLinkHandler', - (res) => { - eventSubscription.remove(); - if (completionHandler) { - completionHandler(res.data, res.error && res.error.length ? res.error: undefined); - } - }); - SingularBridge.createReferrerShortLink(baseLink, referrerName, referrerId, JSON.stringify(passthroughParams)); - } + if (this._didSetSdidCallback) { + this._singularNativeEmitter.addListener('DidSetSdidCallback', + (result) => this._didSetSdidCallback?.(result)); + } + + if (this._sdidReceivedCallback) { + this._singularNativeEmitter.addListener('SdidReceivedCallback', + (result) => this._sdidReceivedCallback?.(result)); + } + } + + static createReferrerShortLink(baseLink, referrerName, referrerId, passthroughParams, completionHandler) { + if (isNewArch) { + SingularBridge.createReferrerShortLink(baseLink, referrerName, referrerId, passthroughParams, completionHandler); + } else { + const subscription = this._singularNativeEmitter.addListener( + 'ShortLinkHandler', + (res) => { + subscription.remove(); + if (completionHandler) { + completionHandler(res.data, res.error && res.error.length ? res.error: undefined); + } + }); + SingularBridge.createReferrerShortLink(baseLink, referrerName, referrerId, JSON.stringify(passthroughParams)); + } + } static setCustomUserId(customUserId) { SingularBridge.setCustomUserId(customUserId); @@ -101,7 +167,11 @@ export class Singular { } static eventWithArgs(eventName, args) { - SingularBridge.eventWithArgs(eventName, JSON.stringify(args)); + if (isNewArch) { + SingularBridge.eventWithArgs(eventName, args); + } else { + SingularBridge.eventWithArgs(eventName, JSON.stringify(args)); + } } static revenue(currency, amount) { @@ -109,7 +179,11 @@ export class Singular { } static revenueWithArgs(currency, amount, args) { - SingularBridge.revenueWithArgs(currency, amount, JSON.stringify(args)); + if (isNewArch) { + SingularBridge.revenueWithArgs(currency, amount, args); + } else { + SingularBridge.revenueWithArgs(currency, amount, JSON.stringify(args)); + } } static customRevenue(eventName, currency, amount) { @@ -117,24 +191,51 @@ export class Singular { } static customRevenueWithArgs(eventName, currency, amount, args) { - SingularBridge.customRevenueWithArgs( - eventName, - currency, - amount, - JSON.stringify(args), - ); + if (isNewArch) { + SingularBridge.customRevenueWithArgs(eventName, currency, amount, args); + } else { + SingularBridge.customRevenueWithArgs(eventName, currency, amount, JSON.stringify(args)); + } } - // This method will report revenue to Singular and will perform receipt validation (if enabled) on our backend. - // The purchase object should be of SingularIOSPurchase / SingularAndroidPurchase type. static inAppPurchase(eventName, purchase) { - this.eventWithArgs(eventName, purchase.getPurchaseValues()); + if (isNewArch) { + if (purchase && typeof purchase.toSpecObject === 'function') { + SingularBridge.inAppPurchase(eventName, purchase.toSpecObject()); + } else { + const convertedPurchase = { + revenue: purchase.r || purchase.revenue, + currency: purchase.pcc || purchase.currency, + productId: purchase.pk || purchase.productId, + transactionId: purchase.pti || purchase.transactionId, + receipt: purchase.ptr || purchase.receipt, + receipt_signature: purchase.receipt_signature + }; + SingularBridge.inAppPurchase(eventName, convertedPurchase); + } + } else { + this.eventWithArgs(eventName, purchase.getPurchaseValues()); + } } - // This method will report revenue to Singular and will perform receipt validation (if enabled) on our backend. - // The purchase object should be of SingularIOSPurchase / SingularAndroidPurchase type. static inAppPurchaseWithArgs(eventName, purchase, args) { - this.eventWithArgs(eventName, {...purchase.getPurchaseValues(), args}); + if (isNewArch) { + if (purchase && typeof purchase.toSpecObject === 'function') { + SingularBridge.inAppPurchaseWithArgs(eventName, purchase.toSpecObject(), args); + } else { + const convertedPurchase = { + revenue: purchase.r || purchase.revenue, + currency: purchase.pcc || purchase.currency, + productId: purchase.pk || purchase.productId, + transactionId: purchase.pti || purchase.transactionId, + receipt: purchase.ptr || purchase.receipt, + receipt_signature: purchase.receipt_signature + }; + SingularBridge.inAppPurchaseWithArgs(eventName, convertedPurchase, args); + } + } else { + this.eventWithArgs(eventName, {...purchase.getPurchaseValues(), ...args}); + } } static setUninstallToken(token) { @@ -176,7 +277,7 @@ export class Singular { } return true } - + static skanUpdateConversionValues(conversionValue, coarse, lock) { if (Platform.OS === 'ios') { SingularBridge.skanUpdateConversionValues(conversionValue, coarse, lock); @@ -197,10 +298,28 @@ export class Singular { } static adRevenue(adData) { - if (!adData || !adData.hasRequiredParams()) { - return; + if (!adData) { + return; + } + + if (typeof adData.hasRequiredParams === 'function') { + if (!adData.hasRequiredParams()) { + return; + } + } + + if (typeof adData.hasRequiredParams !== 'function') { + const hasRequiredParams = adData.ad_platform && adData.ad_currency && adData.ad_revenue; + if (!hasRequiredParams) { + return; + } + } + + if (isNewArch) { + SingularBridge.adRevenue(adData); + } else { + this.eventWithArgs(ADMON_REVENUE_EVENT_NAME, adData); } - this.eventWithArgs(ADMON_REVENUE_EVENT_NAME, adData); } static setGlobalProperty(key, value,overrideExisting) { @@ -229,3 +348,5 @@ export class Singular { SingularBridge.setLimitAdvertisingIdentifiers(enabled); } } + +export { SingularBridge }; diff --git a/SingularPurchase.js b/SingularPurchase.js index 0f2ce11..49cd753 100644 --- a/SingularPurchase.js +++ b/SingularPurchase.js @@ -10,6 +10,13 @@ export class SingularPurchase { getPurchaseValues() { return this._values; } + + toSpecObject() { + return { + revenue: this._values.r, + currency: this._values.pcc, + }; + } } export class SingularIOSPurchase extends SingularPurchase { @@ -22,6 +29,15 @@ export class SingularIOSPurchase extends SingularPurchase { ptr: receipt, }; } + + toSpecObject() { + return { + ...super.toSpecObject(), + productId: this._values.pk, + transactionId: this._values.pti, + receipt: this._values.ptr + }; + } } export class SingularAndroidPurchase extends SingularPurchase { @@ -34,4 +50,12 @@ export class SingularAndroidPurchase extends SingularPurchase { ptr: receipt, }; } + + toSpecObject() { + return { + ...super.toSpecObject(), + receipt: this._values.receipt, + receipt_signature: this._values.receipt_signature + }; + } } diff --git a/android/build.gradle b/android/build.gradle index 5f1dec8..003e427 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,32 +1,41 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.4.2' - } +def isNewArchitectureEnabled() { + return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true" } apply plugin: 'com.android.library' +if (isNewArchitectureEnabled()) { + apply plugin: 'com.facebook.react' +} + android { compileSdkVersion 31 buildToolsVersion "28.0.3" + namespace "net.singular.react_native" defaultConfig { - minSdkVersion 16 + minSdkVersion 21 targetSdkVersion 28 versionCode 1 versionName "1.0" + buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() } + lintOptions { abortOnError false } + + sourceSets { + main { + if (isNewArchitectureEnabled()) { + java.srcDirs = ['src/main/java', 'src/newarch/java', 'build/generated/source/codegen/java'] + } else { + java.srcDirs = ['src/main/java', 'src/oldarch/java'] + } + } + } } repositories { @@ -34,13 +43,20 @@ repositories { url "../node_modules/react-native/android" } mavenCentral() - jcenter() maven { url 'https://maven.singular.net/' } google() } dependencies { implementation 'com.facebook.react:react-native:+' - implementation 'com.singular.sdk:singular_sdk:12.9.1' + implementation 'com.singular.sdk:singular_sdk:12.10.0' implementation 'com.android.support:support-annotations:28.0.0' } + +if (isNewArchitectureEnabled()) { + react { + jsRootDir = file("../js/") + libraryName = "NativeSingular" + codegenJavaPackageName = "net.singular.react_native" + } +} diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 5c2d1cf..0000000 Binary files a/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index e0c4de3..0000000 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew deleted file mode 100755 index b0d6d0a..0000000 --- a/android/gradlew +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat deleted file mode 100644 index 15e1ee3..0000000 --- a/android/gradlew.bat +++ /dev/null @@ -1,100 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem http://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/android/src/main/java/net/singular/react_native/SingularBridgePackage.java b/android/src/main/java/net/singular/react_native/SingularBridgePackage.java index 44a2b63..416f55d 100644 --- a/android/src/main/java/net/singular/react_native/SingularBridgePackage.java +++ b/android/src/main/java/net/singular/react_native/SingularBridgePackage.java @@ -2,20 +2,23 @@ import androidx.annotation.NonNull; -import com.facebook.react.ReactPackage; -import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.ReactPackage; import com.facebook.react.uimanager.ViewManager; +import net.singular.react_native.SingularBridgeModule; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class SingularBridgePackage implements ReactPackage { + @Override public List createNativeModules(ReactApplicationContext reactContext) { List nativeModules = new ArrayList<>(); + // Add SingularBridgeModule - the correct implementation is auto-selected by build.gradle + // based on isNewArchitectureEnabled() nativeModules.add(new SingularBridgeModule(reactContext)); return nativeModules; } @@ -26,3 +29,4 @@ public List createViewManagers(@NonNull ReactApplicationContext rea return Collections.emptyList(); } } + diff --git a/android/src/main/java/net/singular/react_native/SingularHelper.java b/android/src/main/java/net/singular/react_native/SingularHelper.java new file mode 100644 index 0000000..1ec9618 --- /dev/null +++ b/android/src/main/java/net/singular/react_native/SingularHelper.java @@ -0,0 +1,135 @@ +package net.singular.react_native; +import com.singular.sdk.Singular; +import com.singular.sdk.SingularConfig; +import com.singular.sdk.ShortLinkHandler; +import org.json.JSONObject; +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.Arrays; +import com.facebook.react.bridge.ReactApplicationContext; + +public class SingularHelper { + + protected interface Constants { + String SINGULAR_LINK_HANDLER_CONST = "SingularLinkHandler"; + String DEVICE_ATTRIBUTION_CALLBACK_HANDLER_CONST = "DeviceAttributionCallbackHandler"; + String SHORT_LINK_HANDLER_CONST = "ShortLinkHandler"; + String DID_SET_SDID_HANDLER_CONST = "DidSetSdidCallback"; + String SDID_RECEIVED_HANDLER_CONST = "SdidReceivedCallback"; + } + + public static void initWithSingularConfig(ReactApplicationContext context, SingularConfig config) { + Singular.init(context, config); + } + + public static void setCustomUserId(String customUserId) { + Singular.setCustomUserId(customUserId); + } + + public static void unsetCustomUserId() { + Singular.unsetCustomUserId(); + } + + public static void setDeviceCustomUserId(String customUserId) { + Singular.setDeviceCustomUserId(customUserId); + } + + public static void event(String name) { + Singular.event(name); + } + + public static void eventWithArgs(String name, String extra) { + Singular.event(name, extra); + } + + public static void revenue(String currency, double amount) { + Singular.revenue(currency, amount); + } + + public static void revenueWithArgs(String currency, double amount, Map args) { + Singular.revenue(currency, amount, args); + } + + public static void customRevenue(String eventName, String currency, double amount) { + Singular.customRevenue(eventName, currency, amount); + } + + public static void customRevenueWithArgs(String eventName, String currency, double amount, Map args) { + Singular.customRevenue(eventName, currency, amount, args); + } + + public static void setUninstallToken(String token) { + Singular.setFCMDeviceToken(token); + } + + public static void trackingOptIn() { + Singular.trackingOptIn(); + } + + public static void trackingUnder13() { + Singular.trackingUnder13(); + } + + public static void stopAllTracking() { + Singular.stopAllTracking(); + } + + public static void resumeAllTracking() { + Singular.resumeAllTracking(); + } + + public static boolean isAllTrackingStopped() { + return Singular.isAllTrackingStopped(); + } + + public static void limitDataSharing(boolean limitDataSharingValue) { + Singular.limitDataSharing(limitDataSharingValue); + } + + public static boolean getLimitDataSharing() { + return Singular.getLimitDataSharing(); + } + + public static void setReactSDKVersion(String wrapper, String version) { + Singular.setWrapperNameAndVersion(wrapper, version); + } + + public static boolean setGlobalProperty(String key, String value, boolean overrideExisting) { + return Singular.setGlobalProperty(key, value, overrideExisting); + } + + public static void unsetGlobalProperty(String key) { + Singular.unsetGlobalProperty(key); + } + + public static void clearGlobalProperties() { + Singular.clearGlobalProperties(); + } + + public static Map getGlobalProperties() { + return Singular.getGlobalProperties(); + } + + public static void setLimitAdvertisingIdentifiers(boolean enabled) { + Singular.setLimitAdvertisingIdentifiers(enabled); + } + + public static void createReferrerShortLink(String baseLink, + String referrerName, + String referrerId, + JSONObject passthroughParams, + ShortLinkHandler completionHandler) { + Singular.createReferrerShortLink(baseLink, referrerName, referrerId, passthroughParams, completionHandler); + } + + public static boolean isValidNonEmptyString(String str) { + return str != null + && str instanceof String + && str.length() > 0 + && !str.toLowerCase().equals("null") + && !str.toLowerCase().equals("undefined") + && !str.toLowerCase().equals("false") + && !str.equals("NaN"); + } +} diff --git a/android/src/newarch/java/net/singular/react_native/SingularBridgeModule.java b/android/src/newarch/java/net/singular/react_native/SingularBridgeModule.java new file mode 100644 index 0000000..9150cb5 --- /dev/null +++ b/android/src/newarch/java/net/singular/react_native/SingularBridgeModule.java @@ -0,0 +1,1000 @@ +package net.singular.react_native; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableType; +import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +import android.util.Log; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import android.content.Intent; +import org.json.JSONObject; +import org.json.JSONArray; + +import com.singular.sdk.Singular; +import com.singular.sdk.SingularConfig; +import com.singular.sdk.SingularDeviceAttributionHandler; +import com.singular.sdk.SingularLinkHandler; +import com.singular.sdk.SingularLinkParams; +import com.singular.sdk.SDIDAccessorHandler; +import com.singular.sdk.ShortLinkHandler; + + +public class SingularBridgeModule extends NativeSingularSpec { + public static final String NAME = "SingularBridge"; + private static final String version = "4.0.0"; + private static final String wrapper = "ReactNative"; + + private static SingularConfig config; + private static int currentIntentHash; + private long shortLinkResolveTimeout = 10; + + private static String[][] pushNotificationsLinkPaths; + private static SingularLinkHandler staticSingularLinkHandler; + private static ReactApplicationContext reactContext; + + private static final JSONObject EMPTY_JSON_OBJECT = new JSONObject(); + private static final JSONArray EMPTY_JSON_ARRAY = new JSONArray(); + + private interface Constants { + // Config key constants + String CONFIG_KEY_APIKEY = "apikey"; + String CONFIG_KEY_SECRET = "secret"; + String CONFIG_KEY_ESP_DOMAINS = "espDomains"; + String CONFIG_KEY_SESSION_TIMEOUT = "sessionTimeout"; + String CONFIG_KEY_DDL_TIMEOUT_SEC = "ddlTimeoutSec"; + String CONFIG_KEY_SHORT_LINK_RESOLVE_TIMEOUT = "shortLinkResolveTimeout"; + String CONFIG_KEY_CUSTOM_USER_ID = "customUserId"; + String CONFIG_KEY_IMEI = "imei"; + String CONFIG_KEY_LIMIT_DATA_SHARING = "limitDataSharing"; + String CONFIG_KEY_COLLECT_OAID = "collectOAID"; + String CONFIG_KEY_ENABLE_LOGGING = "enableLogging"; + String CONFIG_KEY_LIMIT_ADVERTISING_IDENTIFIERS = "limitAdvertisingIdentifiers"; + String CONFIG_KEY_LOG_LEVEL = "logLevel"; + String CONFIG_KEY_FACEBOOK_APP_ID = "facebookAppId"; + String CONFIG_KEY_GLOBAL_PROPERTIES = "globalProperties"; + String CONFIG_KEY_CUSTOM_SDID = "customSdid"; + String CONFIG_KEY_BRANDED_DOMAINS = "brandedDomains"; + String CONFIG_KEY_PUSH_NOTIFICATIONS_LINK_PATHS = "pushNotificationsLinkPaths"; + + // Global properties key constants + String GLOBAL_PROP_KEY = "Key"; + String GLOBAL_PROP_VALUE = "Value"; + String GLOBAL_PROP_OVERRIDE_EXISTING = "OverrideExisting"; + + // Ad Revenue key constants + interface AdRevenue { + String AD_REVENUE_EVENT = "__ADMON_USER_LEVEL_REVENUE__"; + String AD_PLATFORM = "ad_platform"; + String AD_CURRENCY = "ad_currency"; + String AD_REVENUE = "ad_revenue"; + String R = "r"; + String PCC = "pcc"; + String IS_ADMON_REVENUE = "is_admon_revenue"; + String IS_REVENUE_EVENT = "is_revenue_event"; + String AD_MEDIATION_PLATFORM = "ad_mediation_platform"; + String AD_TYPE = "ad_type"; + String AD_GROUP_TYPE = "ad_group_type"; + String AD_IMPRESSION_ID = "ad_impression_id"; + String AD_PLACEMENT_NAME = "ad_placement_name"; + String AD_UNIT_ID = "ad_unit_id"; + String AD_UNIT_NAME = "ad_unit_name"; + String AD_GROUP_ID = "ad_group_id"; + String AD_GROUP_NAME = "ad_group_name"; + String AD_GROUP_PRIORITY = "ad_group_priority"; + String AD_PRECISION = "ad_precision"; + String AD_PLACEMENT_ID = "ad_placement_id"; + } + } + + private String convertToJsonString(ReadableMap map) { + if (map == null) return null; + try { + return convertReadableMapToJSONObject(map).toString(); + } catch (Exception e) { + return null; + } + } + + @Override + public String getName() { + return NAME; + } + + public SingularBridgeModule(ReactApplicationContext context) { + super(context); + reactContext = context; + } + + @Override + public void init(ReadableMap configMap) { + try { + config = buildSingularConfigFromReadableMap(configMap); + if (config != null) { + SingularHelper.initWithSingularConfig(reactContext, config); + SingularHelper.setReactSDKVersion(wrapper, version); + } + } catch (Exception e) { + Log.e("SingularSDK", "Error during init", e); + } + } + + // Required for event emission in TurboModules + @Override + public void addListener(String eventType) { + // Keep: Required for RN built in Event Emitter Calls. + } + + @Override + public void removeListeners(double count) { + // Keep: Required for RN built in Event Emitter Calls. + } + + @Override + public void event(String eventName) { + SingularHelper.event(eventName); + } + + @Override + public void eventWithArgs(String eventName, ReadableMap args) { + String argsString = convertToJsonString(args); + SingularHelper.eventWithArgs(eventName, argsString); + } + + @Override + public void setCustomUserId(String customUserId) { + SingularHelper.setCustomUserId(customUserId); + } + + @Override + public void unsetCustomUserId() { + SingularHelper.unsetCustomUserId(); + } + + @Override + public void setDeviceCustomUserId(String customUserId) { + SingularHelper.setDeviceCustomUserId(customUserId); + } + + @Override + public void revenue(String currency, double amount) { + SingularHelper.revenue(currency, amount); + } + + @Override + public void revenueWithArgs(String currency, double amount, ReadableMap args) { + Map argsMap = convertReadableMapToMap(args); + SingularHelper.revenueWithArgs(currency, amount, argsMap); + } + + @Override + public void customRevenue(String eventName, String currency, double amount) { + SingularHelper.customRevenue(eventName, currency, amount); + } + + @Override + public void customRevenueWithArgs(String eventName, String currency, double amount, ReadableMap args) { + Map argsMap = convertReadableMapToMap(args); + SingularHelper.customRevenueWithArgs(eventName, currency, amount, argsMap); + } + + @Override + public void inAppPurchase(String eventName, ReadableMap purchase) { + if (purchase == null) return; + + try { + Map purchaseValues = getPurchaseValues(purchase); + if (purchaseValues != null && !purchaseValues.isEmpty()) { + JSONObject jsonObject = new JSONObject(purchaseValues); + SingularHelper.eventWithArgs(eventName, jsonObject.toString()); + } + } catch (Exception e) { + } + } + + @Override + public void inAppPurchaseWithArgs(String eventName, ReadableMap purchase, ReadableMap args) { + if (purchase == null) return; + + try { + Map purchaseValues = getPurchaseValues(purchase); + if (purchaseValues == null || purchaseValues.isEmpty()) return; + + if (args != null) { + Map argsMap = convertReadableMapToMap(args); + if (argsMap != null && !argsMap.isEmpty()) { + purchaseValues.putAll(argsMap); + } + } + + JSONObject jsonObject = new JSONObject(purchaseValues); + SingularHelper.eventWithArgs(eventName, jsonObject.toString()); + } catch (Exception e) { + } + } + + @Override + public void setUninstallToken(String token) { + SingularHelper.setUninstallToken(token); + } + + @Override + public void trackingOptIn() { + SingularHelper.trackingOptIn(); + } + + @Override + public void trackingUnder13() { + SingularHelper.trackingUnder13(); + } + + @Override + public void stopAllTracking() { + SingularHelper.stopAllTracking(); + } + + @Override + public void resumeAllTracking() { + SingularHelper.resumeAllTracking(); + } + + @Override + public boolean isAllTrackingStopped() { + return SingularHelper.isAllTrackingStopped(); + } + + @Override + public void limitDataSharing(boolean shouldLimitDataSharing) { + SingularHelper.limitDataSharing(shouldLimitDataSharing); + } + + @Override + public boolean getLimitDataSharing() { + return SingularHelper.getLimitDataSharing(); + } + + @Override + public boolean setGlobalProperty(String key, String value, boolean overrideExisting) { + return SingularHelper.setGlobalProperty(key, value, overrideExisting); + } + + @Override + public void unsetGlobalProperty(String key) { + SingularHelper.unsetGlobalProperty(key); + } + + @Override + public void clearGlobalProperties() { + SingularHelper.clearGlobalProperties(); + } + + @Override + public WritableMap getGlobalProperties() { + Map globalProps = SingularHelper.getGlobalProperties(); + return convertStringMapToWritableMap(globalProps); + } + + @Override + public void createReferrerShortLink(String baseLink, String referrerName, String referrerId, ReadableMap passthroughParams, Callback completionHandler) { + try { + JSONObject params = null; + if (passthroughParams != null) { + params = convertReadableMapToJsonObject(passthroughParams); + } + + SingularHelper.createReferrerShortLink(baseLink, + referrerName, + referrerId, + params, + new ShortLinkHandler() { + @Override + public void onSuccess(final String link) { + if (completionHandler != null) { + completionHandler.invoke(link, ""); + } + } + + @Override + public void onError(final String error) { + if (completionHandler != null) { + completionHandler.invoke("", error); + } + } + }); + } catch (Exception e) { + if (completionHandler != null) { + completionHandler.invoke("", "Error: " + e.getMessage()); + } + } + } + + @Override + public void adRevenue(ReadableMap adData) { + try { + Map adRevenueData = new HashMap<>(); + + adRevenueData.put(Constants.AdRevenue.AD_PLATFORM, adData.getString(Constants.AdRevenue.AD_PLATFORM)); + adRevenueData.put(Constants.AdRevenue.AD_CURRENCY, adData.getString(Constants.AdRevenue.AD_CURRENCY)); + adRevenueData.put(Constants.AdRevenue.AD_REVENUE, adData.getDouble(Constants.AdRevenue.AD_REVENUE)); + adRevenueData.put(Constants.AdRevenue.R, adData.getDouble(Constants.AdRevenue.AD_REVENUE)); + adRevenueData.put(Constants.AdRevenue.PCC, adData.getString(Constants.AdRevenue.AD_CURRENCY)); + adRevenueData.put(Constants.AdRevenue.IS_ADMON_REVENUE, true); + adRevenueData.put(Constants.AdRevenue.IS_REVENUE_EVENT, true); + + if (adData.hasKey(Constants.AdRevenue.AD_MEDIATION_PLATFORM) && !adData.isNull(Constants.AdRevenue.AD_MEDIATION_PLATFORM)) { + adRevenueData.put(Constants.AdRevenue.AD_MEDIATION_PLATFORM, adData.getString(Constants.AdRevenue.AD_MEDIATION_PLATFORM)); + } + if (adData.hasKey(Constants.AdRevenue.AD_TYPE) && !adData.isNull(Constants.AdRevenue.AD_TYPE)) { + adRevenueData.put(Constants.AdRevenue.AD_TYPE, adData.getString(Constants.AdRevenue.AD_TYPE)); + } + if (adData.hasKey(Constants.AdRevenue.AD_GROUP_TYPE) && !adData.isNull(Constants.AdRevenue.AD_GROUP_TYPE)) { + adRevenueData.put(Constants.AdRevenue.AD_GROUP_TYPE, adData.getString(Constants.AdRevenue.AD_GROUP_TYPE)); + } + if (adData.hasKey(Constants.AdRevenue.AD_IMPRESSION_ID) && !adData.isNull(Constants.AdRevenue.AD_IMPRESSION_ID)) { + adRevenueData.put(Constants.AdRevenue.AD_IMPRESSION_ID, adData.getString(Constants.AdRevenue.AD_IMPRESSION_ID)); + } + if (adData.hasKey(Constants.AdRevenue.AD_PLACEMENT_NAME) && !adData.isNull(Constants.AdRevenue.AD_PLACEMENT_NAME)) { + adRevenueData.put(Constants.AdRevenue.AD_PLACEMENT_NAME, adData.getString(Constants.AdRevenue.AD_PLACEMENT_NAME)); + } + if (adData.hasKey(Constants.AdRevenue.AD_UNIT_ID) && !adData.isNull(Constants.AdRevenue.AD_UNIT_ID)) { + adRevenueData.put(Constants.AdRevenue.AD_UNIT_ID, adData.getString(Constants.AdRevenue.AD_UNIT_ID)); + } + if (adData.hasKey(Constants.AdRevenue.AD_UNIT_NAME) && !adData.isNull(Constants.AdRevenue.AD_UNIT_NAME)) { + adRevenueData.put(Constants.AdRevenue.AD_UNIT_NAME, adData.getString(Constants.AdRevenue.AD_UNIT_NAME)); + } + if (adData.hasKey(Constants.AdRevenue.AD_GROUP_ID) && !adData.isNull(Constants.AdRevenue.AD_GROUP_ID)) { + adRevenueData.put(Constants.AdRevenue.AD_GROUP_ID, adData.getString(Constants.AdRevenue.AD_GROUP_ID)); + } + if (adData.hasKey(Constants.AdRevenue.AD_GROUP_NAME) && !adData.isNull(Constants.AdRevenue.AD_GROUP_NAME)) { + adRevenueData.put(Constants.AdRevenue.AD_GROUP_NAME, adData.getString(Constants.AdRevenue.AD_GROUP_NAME)); + } + if (adData.hasKey(Constants.AdRevenue.AD_GROUP_PRIORITY) && !adData.isNull(Constants.AdRevenue.AD_GROUP_PRIORITY)) { + adRevenueData.put(Constants.AdRevenue.AD_GROUP_PRIORITY, adData.getDouble(Constants.AdRevenue.AD_GROUP_PRIORITY)); + } + if (adData.hasKey(Constants.AdRevenue.AD_PRECISION) && !adData.isNull(Constants.AdRevenue.AD_PRECISION)) { + adRevenueData.put(Constants.AdRevenue.AD_PRECISION, adData.getString(Constants.AdRevenue.AD_PRECISION)); + } + if (adData.hasKey(Constants.AdRevenue.AD_PLACEMENT_ID) && !adData.isNull(Constants.AdRevenue.AD_PLACEMENT_ID)) { + adRevenueData.put(Constants.AdRevenue.AD_PLACEMENT_ID, adData.getString(Constants.AdRevenue.AD_PLACEMENT_ID)); + } + + JSONObject jsonObject = new JSONObject(adRevenueData); + String adRevenueString = jsonObject.toString(); + Singular.event(Constants.AdRevenue.AD_REVENUE_EVENT, adRevenueString); + } catch (Exception e) { + } + } + + @Override + public void setLimitAdvertisingIdentifiers(boolean enabled) { + SingularHelper.setLimitAdvertisingIdentifiers(enabled); + } + + // iOS-specific methods (no-op on Android, but required for Codegen) + @Override + public boolean skanUpdateConversionValue(double conversionValue) { + // SKAdNetwork is iOS-only, no-op on Android + return false; + } + + @Override + public void skanUpdateConversionValues(double conversionValue, double coarse, boolean lock) { + // SKAdNetwork is iOS-only, no-op on Android + } + + @Override + public Double skanGetConversionValue() { + // SKAdNetwork is iOS-only, return null on Android + return null; + } + + @Override + public void skanRegisterAppForAdNetworkAttribution() { + // SKAdNetwork is iOS-only, no-op on Android + } + + @Override + public void handlePushNotification(ReadableMap pushNotificationPayload) { + // Push notification handling is iOS-specific, no-op on Android + // Android handles push notifications differently + } + + private SDIDAccessorHandler createSdidHandler() { + return new SDIDAccessorHandler() { + @Override + public void didSetSdid(String result) { + reactContext. + getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(SingularHelper.Constants.DID_SET_SDID_HANDLER_CONST, result); + } + + @Override + public void sdidReceived(String result) { + reactContext. + getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(SingularHelper.Constants.SDID_RECEIVED_HANDLER_CONST, result); + } + }; + } + + private SingularConfig buildSingularConfigFromReadableMap(ReadableMap configMap) { + try { + String apikey = configMap.getString(Constants.CONFIG_KEY_APIKEY); + String secret = configMap.getString(Constants.CONFIG_KEY_SECRET); + SingularConfig config = new SingularConfig(apikey, secret); + handleConfigOptionalFields(configMap, config); + setupCallbacks(config); + return config; + } catch (Exception e) { + return null; + } + } + + private void handleConfigOptionalFields(ReadableMap configMap, SingularConfig config) { + if (configMap.hasKey(Constants.CONFIG_KEY_ESP_DOMAINS)) { + ReadableArray espDomainsArray = configMap.getArray(Constants.CONFIG_KEY_ESP_DOMAINS); + List espDomainsList = convertReadableArrayToList(espDomainsArray); + if (espDomainsList != null && espDomainsList.size() > 0) { + config.withESPDomains(espDomainsList); + } + } + + int sessionTimeout = -1; + if (configMap.hasKey(Constants.CONFIG_KEY_SESSION_TIMEOUT)) { + sessionTimeout = configMap.getInt(Constants.CONFIG_KEY_SESSION_TIMEOUT); + } + if (sessionTimeout >= 0) { + config.withSessionTimeoutInSec(sessionTimeout); + } + + long ddlTimeoutSec = 0; + if (configMap.hasKey(Constants.CONFIG_KEY_DDL_TIMEOUT_SEC)) { + ddlTimeoutSec = configMap.getInt(Constants.CONFIG_KEY_DDL_TIMEOUT_SEC); + } + if (ddlTimeoutSec > 0) { + config.withDDLTimeoutInSec(ddlTimeoutSec); + } + + if (configMap.hasKey(Constants.CONFIG_KEY_SHORT_LINK_RESOLVE_TIMEOUT)) { + shortLinkResolveTimeout = configMap.getInt(Constants.CONFIG_KEY_SHORT_LINK_RESOLVE_TIMEOUT); + } + + if (configMap.hasKey(Constants.CONFIG_KEY_CUSTOM_USER_ID)) { + String customUserId = configMap.getString(Constants.CONFIG_KEY_CUSTOM_USER_ID); + if (customUserId != null) { + config.withCustomUserId(customUserId); + } + } + + if (configMap.hasKey(Constants.CONFIG_KEY_IMEI)) { + String imei = configMap.getString(Constants.CONFIG_KEY_IMEI); + if (imei != null) { + config.withIMEI(imei); + } + } + + if (configMap.hasKey(Constants.CONFIG_KEY_LIMIT_DATA_SHARING) && !configMap.isNull(Constants.CONFIG_KEY_LIMIT_DATA_SHARING)) { + boolean limitDataSharing = configMap.getBoolean(Constants.CONFIG_KEY_LIMIT_DATA_SHARING); + config.withLimitDataSharing(limitDataSharing); + } + + if (configMap.hasKey(Constants.CONFIG_KEY_COLLECT_OAID)) { + boolean collectOAID = configMap.getBoolean(Constants.CONFIG_KEY_COLLECT_OAID); + if (collectOAID) { + config.withOAIDCollection(); + } + } + + if (configMap.hasKey(Constants.CONFIG_KEY_ENABLE_LOGGING)) { + boolean enableLogging = configMap.getBoolean(Constants.CONFIG_KEY_ENABLE_LOGGING); + if (enableLogging) { + config.withLoggingEnabled(); + } + } + + if (configMap.hasKey(Constants.CONFIG_KEY_LIMIT_ADVERTISING_IDENTIFIERS)) { + boolean limitAdvertisingIdentifiers = configMap.getBoolean(Constants.CONFIG_KEY_LIMIT_ADVERTISING_IDENTIFIERS); + if (limitAdvertisingIdentifiers) { + config.withLimitAdvertisingIdentifiers(); + } + } + + int logLevel = -1; + if (configMap.hasKey(Constants.CONFIG_KEY_LOG_LEVEL)) { + logLevel = configMap.getInt(Constants.CONFIG_KEY_LOG_LEVEL); + } + + if (logLevel >= 0) { + config.withLogLevel(logLevel); + } + + if (configMap.hasKey(Constants.CONFIG_KEY_FACEBOOK_APP_ID)) { + String facebookAppId = configMap.getString(Constants.CONFIG_KEY_FACEBOOK_APP_ID); + if (facebookAppId != null) { + config.withFacebookAppId(facebookAppId); + } + } + + if (configMap.hasKey(Constants.CONFIG_KEY_GLOBAL_PROPERTIES)) { + ReadableMap globalProperties = configMap.getMap(Constants.CONFIG_KEY_GLOBAL_PROPERTIES); + if (globalProperties != null) { + handleGlobalProperties(globalProperties, config); + } + } + + String customSdid = null; + if (configMap.hasKey(Constants.CONFIG_KEY_CUSTOM_SDID)) { + customSdid = configMap.getString(Constants.CONFIG_KEY_CUSTOM_SDID); + if (!SingularHelper.isValidNonEmptyString(customSdid)) { + customSdid = null; + } + } + + config.withCustomSdid(customSdid, createSdidHandler()); + + if (configMap.hasKey(Constants.CONFIG_KEY_BRANDED_DOMAINS)) { + ReadableArray brandedDomainsArray = configMap.getArray(Constants.CONFIG_KEY_BRANDED_DOMAINS); + List brandedDomainsList = convertReadableArrayToList(brandedDomainsArray); + if (brandedDomainsList != null && brandedDomainsList.size() > 0) { + config.withBrandedDomains(brandedDomainsList); + } + } + + if (configMap.hasKey(Constants.CONFIG_KEY_PUSH_NOTIFICATIONS_LINK_PATHS)) { + ReadableArray pushNotificationLinkPathsArray = configMap.getArray(Constants.CONFIG_KEY_PUSH_NOTIFICATIONS_LINK_PATHS); + String[][] pushSelectors = convertReadableArrayTo2DArray(pushNotificationLinkPathsArray); + if (pushSelectors != null) { + pushNotificationsLinkPaths = pushSelectors; + } + } + } + + private void handleGlobalProperties(ReadableMap globalProperties, SingularConfig config) { + if (globalProperties == null) return; + + ReadableMapKeySetIterator iterator = globalProperties.keySetIterator(); + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + ReadableMap property = globalProperties.getMap(key); + + if (property != null) { + String propertyKey = property.getString(Constants.GLOBAL_PROP_KEY); + String propertyValue = property.getString(Constants.GLOBAL_PROP_VALUE); + boolean overrideExisting = false; + if (property.hasKey(Constants.GLOBAL_PROP_OVERRIDE_EXISTING)) { + overrideExisting = property.getBoolean(Constants.GLOBAL_PROP_OVERRIDE_EXISTING); + } + + if (propertyKey != null && propertyValue != null) { + config.withGlobalProperty(propertyKey, propertyValue, overrideExisting); + } + } + } + } + + private void setupCallbacks(SingularConfig config) { + config.withSingularDeviceAttribution(new SingularDeviceAttributionHandler() { + @Override + public void onDeviceAttributionInfoReceived(Map deviceAttributionData) { + WritableMap attributionInfo = convertMapToWritableMap(deviceAttributionData); + reactContext. + getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(SingularHelper.Constants.DEVICE_ATTRIBUTION_CALLBACK_HANDLER_CONST, attributionInfo); + } + }); + + staticSingularLinkHandler = new SingularLinkHandler() { + @Override + public void onResolved(SingularLinkParams singularLinkParams) { + WritableMap params = createSingularLinkParams(singularLinkParams); + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(SingularHelper.Constants.SINGULAR_LINK_HANDLER_CONST, params); + } + }; + + if (reactContext.hasCurrentActivity()) { + Intent intent = getCurrentActivity().getIntent(); + if (intent != null) { + int intentHash = intent.hashCode(); + if (intentHash != currentIntentHash) { + currentIntentHash = intentHash; + config.withSingularLink(intent, staticSingularLinkHandler, shortLinkResolveTimeout); + if (intent.getExtras() != null && intent.getExtras().size() > 0 && pushNotificationsLinkPaths != null) { + config.withPushNotificationPayload(intent, pushNotificationsLinkPaths); + } + } + } + } else { + config.withSingularLink(null, staticSingularLinkHandler, shortLinkResolveTimeout); + } + } + + private List convertReadableArrayToList(ReadableArray readableArray) { + if (readableArray == null) return null; + + List list = new ArrayList<>(); + for (int i = 0; i < readableArray.size(); i++) { + if (readableArray.getType(i) == ReadableType.String) { + String item = readableArray.getString(i); + if (item != null && !item.isEmpty()) { + list.add(item); + } + } + } + return list.isEmpty() ? null : list; + } + + private String[][] convertReadableArrayTo2DArray(ReadableArray readableArray) { + if (readableArray == null || readableArray.size() <= 0) { + return null; + } + + try { + String[][] result = new String[readableArray.size()][]; + + for (int outerIndex = 0; outerIndex < readableArray.size(); outerIndex++) { + if (readableArray.getType(outerIndex) == ReadableType.Array) { + ReadableArray innerArray = readableArray.getArray(outerIndex); + result[outerIndex] = new String[innerArray.size()]; + + for (int innerIndex = 0; innerIndex < innerArray.size(); innerIndex++) { + if (innerArray.getType(innerIndex) == ReadableType.String) { + result[outerIndex][innerIndex] = innerArray.getString(innerIndex); + } + } + } + } + + return result; + } catch (Exception e) { + return null; + } + } + + private WritableMap convertMapToWritableMap(Map map) { + WritableMap writableMap = Arguments.createMap(); + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (value instanceof String) { + writableMap.putString(key, (String) value); + } else if (value instanceof Integer) { + writableMap.putInt(key, (Integer) value); + } else if (value instanceof Double) { + writableMap.putDouble(key, (Double) value); + } else if (value instanceof Boolean) { + writableMap.putBoolean(key, (Boolean) value); + } else { + writableMap.putString(key, value.toString()); + } + } + return writableMap; + } + + private WritableMap createSingularLinkParams(SingularLinkParams singularLinkParams) { + WritableMap params = Arguments.createMap(); + params.putString("deeplink", singularLinkParams.getDeeplink()); + params.putString("passthrough", singularLinkParams.getPassthrough()); + params.putBoolean("isDeferred", singularLinkParams.isDeferred()); + + WritableMap urlParams = Arguments.createMap(); + if (singularLinkParams.getUrlParameters() != null) { + for (Map.Entry entry : singularLinkParams.getUrlParameters().entrySet()) { + urlParams.putString(entry.getKey(), entry.getValue()); + } + } + params.putMap("urlParameters", urlParams); + + return params; + } + + private Map convertReadableMapToMap(ReadableMap readableMap) { + if (readableMap == null) return null; + + Map map = new HashMap<>(); + ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); + + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + ReadableType type = readableMap.getType(key); + + switch (type) { + case Null: + map.put(key, null); + break; + case Boolean: + map.put(key, readableMap.getBoolean(key)); + break; + case Number: + map.put(key, readableMap.getDouble(key)); + break; + case String: + map.put(key, readableMap.getString(key)); + break; + case Map: + map.put(key, convertReadableMapToMap(readableMap.getMap(key))); + break; + case Array: + map.put(key, convertReadableArrayToObjectList(readableMap.getArray(key))); + break; + default: + Log.w("SingularSDK", "Unknown ReadableType: " + type); + break; + } + } + + return map; + } + + private JSONObject convertReadableMapToJSONObject(ReadableMap readableMap) throws Exception { + if (readableMap == null) return EMPTY_JSON_OBJECT; + + ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); + if (!iterator.hasNextKey()) return EMPTY_JSON_OBJECT; + + JSONObject jsonObject = new JSONObject(); + + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + ReadableType type = readableMap.getType(key); + + switch (type) { + case Null: + jsonObject.put(key, JSONObject.NULL); + break; + case Boolean: + jsonObject.put(key, readableMap.getBoolean(key)); + break; + case Number: + jsonObject.put(key, readableMap.getDouble(key)); + break; + case String: + jsonObject.put(key, readableMap.getString(key)); + break; + case Map: + jsonObject.put(key, convertReadableMapToJSONObject(readableMap.getMap(key))); + break; + case Array: + jsonObject.put(key, convertReadableArrayToJSONArray(readableMap.getArray(key))); + break; + default: + Log.w("SingularSDK", "Unknown ReadableType: " + type); + break; + } + } + + return jsonObject; + } + + private JSONArray convertReadableArrayToJSONArray(ReadableArray readableArray) throws Exception { + if (readableArray == null) return EMPTY_JSON_ARRAY; + + int size = readableArray.size(); + if (size == 0) return EMPTY_JSON_ARRAY; + + JSONArray jsonArray = new JSONArray(); + for (int i = 0; i < size; i++) { + ReadableType type = readableArray.getType(i); + + switch (type) { + case Null: + jsonArray.put(JSONObject.NULL); + break; + case Boolean: + jsonArray.put(readableArray.getBoolean(i)); + break; + case Number: + jsonArray.put(readableArray.getDouble(i)); + break; + case String: + jsonArray.put(readableArray.getString(i)); + break; + case Map: + jsonArray.put(convertReadableMapToJSONObject(readableArray.getMap(i))); + break; + case Array: + jsonArray.put(convertReadableArrayToJSONArray(readableArray.getArray(i))); + break; + default: + Log.w("SingularSDK", "Unknown ReadableType: " + type); + break; + } + } + + return jsonArray; + } + + private List convertReadableArrayToObjectList(ReadableArray readableArray) { + if (readableArray == null) return null; + + List list = new ArrayList<>(); + for (int i = 0; i < readableArray.size(); i++) { + ReadableType type = readableArray.getType(i); + + switch (type) { + case Null: + list.add(null); + break; + case Boolean: + list.add(readableArray.getBoolean(i)); + break; + case Number: + list.add(readableArray.getDouble(i)); + break; + case String: + list.add(readableArray.getString(i)); + break; + case Map: + list.add(convertReadableMapToMap(readableArray.getMap(i))); + break; + case Array: + list.add(convertReadableArrayToObjectList(readableArray.getArray(i))); + break; + default: + Log.w("SingularSDK", "Unknown ReadableType: " + type); + break; + } + } + + return list; + } + + private WritableMap convertStringMapToWritableMap(Map map) { + WritableMap writableMap = Arguments.createMap(); + + if (map != null) { + for (Map.Entry entry : map.entrySet()) { + writableMap.putString(entry.getKey(), entry.getValue()); + } + } + + return writableMap; + } + + private Map getPurchaseValues(ReadableMap purchase) { + Map purchaseValues = new HashMap<>(); + + if (purchase.hasKey("revenue")) { + purchaseValues.put("r", purchase.getDouble("revenue")); + } + + if (purchase.hasKey("currency")) { + purchaseValues.put("pcc", purchase.getString("currency")); + } + + purchaseValues.put("is_revenue_event", true); + + if (purchase.hasKey("receipt") && !purchase.isNull("receipt")) { + String receiptValue = purchase.getString("receipt"); + purchaseValues.put("receipt", receiptValue); + purchaseValues.put("ptr", receiptValue); + } + + if (purchase.hasKey("receipt_signature") && !purchase.isNull("receipt_signature")) { + purchaseValues.put("receipt_signature", purchase.getString("receipt_signature")); + } + + return purchaseValues; + } + + private JSONObject convertReadableMapToJsonObject(ReadableMap readableMap) { + if (readableMap == null) return null; + + try { + JSONObject jsonObject = new JSONObject(); + ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); + + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + ReadableType type = readableMap.getType(key); + + switch (type) { + case Null: + jsonObject.put(key, JSONObject.NULL); + break; + case Boolean: + jsonObject.put(key, readableMap.getBoolean(key)); + break; + case Number: + jsonObject.put(key, readableMap.getDouble(key)); + break; + case String: + jsonObject.put(key, readableMap.getString(key)); + break; + case Map: + JSONObject nestedObject = convertReadableMapToJsonObject(readableMap.getMap(key)); + jsonObject.put(key, nestedObject); + break; + case Array: + JSONArray nestedArray = convertReadableArrayToJsonArray(readableMap.getArray(key)); + jsonObject.put(key, nestedArray); + break; + default: + Log.w("SingularSDK", "Unknown ReadableType: " + type); + break; + } + } + + return jsonObject; + } catch (Exception e) { + return null; + } + } + + private JSONArray convertReadableArrayToJsonArray(ReadableArray readableArray) { + if (readableArray == null) return null; + + try { + JSONArray jsonArray = new JSONArray(); + for (int i = 0; i < readableArray.size(); i++) { + ReadableType type = readableArray.getType(i); + + switch (type) { + case Null: + jsonArray.put(JSONObject.NULL); + break; + case Boolean: + jsonArray.put(readableArray.getBoolean(i)); + break; + case Number: + jsonArray.put(readableArray.getDouble(i)); + break; + case String: + jsonArray.put(readableArray.getString(i)); + break; + case Map: + JSONObject nestedObject = convertReadableMapToJsonObject(readableArray.getMap(i)); + jsonArray.put(nestedObject); + break; + case Array: + JSONArray nestedArray = convertReadableArrayToJsonArray(readableArray.getArray(i)); + jsonArray.put(nestedArray); + break; + default: + Log.w("SingularSDK", "Unknown ReadableType: " + type); + break; + } + } + + return jsonArray; + } catch (Exception e) { + return null; + } + } + + public static void onNewIntent(Intent intent) { + if (intent == null) { + return; + } + + if (config == null) { + return; + } + + if (intent.hashCode() == currentIntentHash) { + return; + } + + currentIntentHash = intent.hashCode(); + + if (intent.getData() != null && staticSingularLinkHandler != null) { + config.withSingularLink(intent, staticSingularLinkHandler); + } + + if (intent.getExtras() != null && intent.getExtras().size() > 0 && pushNotificationsLinkPaths != null && pushNotificationsLinkPaths.length > 0) { + config.withPushNotificationPayload(intent, pushNotificationsLinkPaths); + } + + SingularHelper.initWithSingularConfig(reactContext, config); + } +} diff --git a/android/src/main/java/net/singular/react_native/SingularBridgeModule.java b/android/src/oldarch/java/net/singular/react_native/SingularBridgeModule.java similarity index 85% rename from android/src/main/java/net/singular/react_native/SingularBridgeModule.java rename to android/src/oldarch/java/net/singular/react_native/SingularBridgeModule.java index 3e4d26f..5e80598 100644 --- a/android/src/main/java/net/singular/react_native/SingularBridgeModule.java +++ b/android/src/oldarch/java/net/singular/react_native/SingularBridgeModule.java @@ -39,13 +39,8 @@ import java.util.Map; public class SingularBridgeModule extends ReactContextBaseJavaModule { - private interface Constants { - String SINGULAR_LINK_HANDLER_CONST = "SingularLinkHandler"; - String DEVICE_ATTRIBUTION_CALLBACK_HANDLER_CONST = "DeviceAttributionCallbackHandler"; - String SHORT_LINK_HANDLER_CONST = "ShortLinkHandler"; - } + public static final String NAME = "SingularBridge"; - public static final String REACT_CLASS = "SingularBridge"; private static ReactApplicationContext reactContext = null; private static SingularConfig config; private static SingularLinkHandler singularLinkHandler; @@ -61,128 +56,128 @@ public SingularBridgeModule(ReactApplicationContext context) { @Override public String getName() { - return REACT_CLASS; + return NAME; } @ReactMethod public void init(String configJsonString) { buildSingularConfig(configJsonString); - Singular.init(reactContext, config); + SingularHelper.initWithSingularConfig(reactContext, config); } @ReactMethod public void setCustomUserId(String customUserId) { - Singular.setCustomUserId(customUserId); + SingularHelper.setCustomUserId(customUserId); } @ReactMethod public void unsetCustomUserId() { - Singular.unsetCustomUserId(); + SingularHelper.unsetCustomUserId(); } @ReactMethod public void setDeviceCustomUserId(String customUserId) { - Singular.setDeviceCustomUserId(customUserId); + SingularHelper.setDeviceCustomUserId(customUserId); } @ReactMethod public void event(String name) { - Singular.event(name); + SingularHelper.event(name); } @ReactMethod public void eventWithArgs(String name, String extra) { - Singular.event(name, extra); + SingularHelper.eventWithArgs(name, extra); } @ReactMethod public void revenue(String currency, double amount) { - Singular.revenue(currency, amount); + SingularHelper.revenue(currency, amount); } @ReactMethod public void revenueWithArgs(String currency, double amount, String args) { - Singular.revenue(currency, amount, convertJsonToMap(args)); + SingularHelper.revenueWithArgs(currency, amount, convertJsonToMap(args)); } @ReactMethod public void customRevenue(String eventName, String currency, double amount) { - Singular.customRevenue(eventName, currency, amount); + SingularHelper.customRevenue(eventName, currency, amount); } @ReactMethod public void customRevenueWithArgs(String eventName, String currency, double amount, String args) { - Singular.customRevenue(eventName, currency, amount, convertJsonToMap(args)); + SingularHelper.customRevenueWithArgs(eventName, currency, amount, convertJsonToMap(args)); } @ReactMethod public void setUninstallToken(String token) { - Singular.setFCMDeviceToken(token); + SingularHelper.setUninstallToken(token); } @ReactMethod public void trackingOptIn() { - Singular.trackingOptIn(); + SingularHelper.trackingOptIn(); } @ReactMethod public void trackingUnder13() { - Singular.trackingUnder13(); + SingularHelper.trackingUnder13(); } @ReactMethod public void stopAllTracking() { - Singular.stopAllTracking(); + SingularHelper.stopAllTracking(); } @ReactMethod public void resumeAllTracking() { - Singular.resumeAllTracking(); + SingularHelper.resumeAllTracking(); } @ReactMethod(isBlockingSynchronousMethod = true) public boolean isAllTrackingStopped() { - return Singular.isAllTrackingStopped(); + return SingularHelper.isAllTrackingStopped(); } @ReactMethod public void limitDataSharing(boolean limitDataSharingValue) { - Singular.limitDataSharing(limitDataSharingValue); + SingularHelper.limitDataSharing(limitDataSharingValue); } @ReactMethod(isBlockingSynchronousMethod = true) public boolean getLimitDataSharing() { - return Singular.getLimitDataSharing(); + return SingularHelper.getLimitDataSharing(); } @ReactMethod public void setReactSDKVersion(String wrapper, String version) { - Singular.setWrapperNameAndVersion(wrapper, version); + SingularHelper.setReactSDKVersion(wrapper, version); } @ReactMethod(isBlockingSynchronousMethod = true) public boolean setGlobalProperty(String key, String value, boolean overrideExisting) { - return Singular.setGlobalProperty(key,value,overrideExisting); + return SingularHelper.setGlobalProperty(key, value, overrideExisting); } @ReactMethod public void unsetGlobalProperty(String key) { - Singular.unsetGlobalProperty(key); + SingularHelper.unsetGlobalProperty(key); } @ReactMethod public void clearGlobalProperties() { - Singular.clearGlobalProperties(); + SingularHelper.clearGlobalProperties(); } @ReactMethod(isBlockingSynchronousMethod = true) public WritableMap getGlobalProperties() { - return toWritableMap(Singular.getGlobalProperties()); + return toWritableMap(SingularHelper.getGlobalProperties()); } @ReactMethod public void setLimitAdvertisingIdentifiers(boolean enabled) { - Singular.setLimitAdvertisingIdentifiers(enabled); + SingularHelper.setLimitAdvertisingIdentifiers(enabled); } private void buildSingularConfig(String configString) { @@ -215,7 +210,7 @@ public void onDeviceAttributionInfoReceived(Map deviceAttributio reactContext. getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(Constants.DEVICE_ATTRIBUTION_CALLBACK_HANDLER_CONST, attributionInfo); + .emit(SingularHelper.Constants.DEVICE_ATTRIBUTION_CALLBACK_HANDLER_CONST, attributionInfo); } catch (Throwable e) { Log.d("Singular", "could not convert json to writable map"); } @@ -242,7 +237,7 @@ public void onResolved(SingularLinkParams singularLinkParams) { // Raising the Singular Link handler in the react-native code reactContext. getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(Constants.SINGULAR_LINK_HANDLER_CONST, params); + .emit(SingularHelper.Constants.SINGULAR_LINK_HANDLER_CONST, params); } }; @@ -253,6 +248,7 @@ public void onResolved(SingularLinkParams singularLinkParams) { pushNotificationsLinkPaths = pushSelectors; } + long shortLinkResolveTimeout = configJson.optLong("shortLinkResolveTimeout", 10); if (reactContext.hasCurrentActivity()) { Intent intent = getCurrentActivity().getIntent(); if (intent != null) { @@ -260,8 +256,6 @@ public void onResolved(SingularLinkParams singularLinkParams) { if (intentHash != currentIntentHash) { currentIntentHash = intentHash; - - long shortLinkResolveTimeout = configJson.optLong("shortLinkResolveTimeout", 10); config.withSingularLink(getCurrentActivity().getIntent(), singularLinkHandler, shortLinkResolveTimeout); if (intent.getExtras() != null && intent.getExtras().size() > 0) { @@ -271,8 +265,10 @@ public void onResolved(SingularLinkParams singularLinkParams) { } } } + } else { + config.withSingularLink(null, singularLinkHandler, shortLinkResolveTimeout); } - + String customUserId = configJson.optString("customUserId", null); if (customUserId != null) { config.withCustomUserId(customUserId); @@ -319,7 +315,6 @@ public void onResolved(SingularLinkParams singularLinkParams) { } JSONObject globalProperties = configJson.optJSONObject("globalProperties"); - // Adding all of the global properties to the singular config if (globalProperties != null) { Iterator iter = globalProperties.keys(); while (iter.hasNext()) { @@ -333,20 +328,20 @@ public void onResolved(SingularLinkParams singularLinkParams) { } String customSdid = configJson.optString("customSdid"); - if (!isValidNonEmptyString(customSdid)) { + if (!SingularHelper.isValidNonEmptyString(customSdid)) { customSdid = null; } config.withCustomSdid(customSdid, new SDIDAccessorHandler() { @Override public void didSetSdid(String result) { reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit("DidSetSdidCallback", result); + .emit(SingularHelper.Constants.DID_SET_SDID_HANDLER_CONST, result); } @Override public void sdidReceived(String result) { reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit("SdidReceivedCallback", result); + .emit(SingularHelper.Constants.SDID_RECEIVED_HANDLER_CONST, result); } }); @@ -509,7 +504,6 @@ public static void onNewIntent(Intent intent) { currentIntentHash = intent.hashCode(); if (intent.getData() != null && singularLinkHandler != null) { - // We want to trigger the singular link handler only if it's registered config.withSingularLink(intent, singularLinkHandler); } @@ -517,7 +511,7 @@ public static void onNewIntent(Intent intent) { config.withPushNotificationPayload(intent, pushNotificationsLinkPaths); } - Singular.init(reactContext, config); + SingularHelper.initWithSingularConfig(reactContext, config); } @ReactMethod @@ -533,7 +527,7 @@ public void createReferrerShortLink(String baseLink, e.printStackTrace(); } - Singular.createReferrerShortLink(baseLink, + SingularHelper.createReferrerShortLink(baseLink, referrerName, referrerId, params, @@ -545,7 +539,7 @@ public void onSuccess(final String link) { params.putString("error", ""); reactContext. getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(Constants.SHORT_LINK_HANDLER_CONST, params); + .emit(SingularHelper.Constants.SHORT_LINK_HANDLER_CONST, params); } @@ -556,26 +550,16 @@ public void onError(final String error) { params.putString("error", error); reactContext. getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(Constants.SHORT_LINK_HANDLER_CONST, params); + .emit(SingularHelper.Constants.SHORT_LINK_HANDLER_CONST, params); } }); } - private boolean isValidNonEmptyString(String nullableJavascriptString) { - return nullableJavascriptString != null - && nullableJavascriptString instanceof String - && nullableJavascriptString.length() > 0 - && !nullableJavascriptString.toLowerCase().equals("null") - && !nullableJavascriptString.toLowerCase().equals("undefined") - && !nullableJavascriptString.toLowerCase().equals("false") - && !nullableJavascriptString.equals("NaN"); - } - @ReactMethod public void addListener(String eventName) { // Keep: Required for RN built in Event Emitter Calls. } - + @ReactMethod public void removeListeners(Integer count) { // Keep: Required for RN built in Event Emitter Calls. diff --git a/index.d.ts b/index.d.ts index 63386a3..acd0427 100644 --- a/index.d.ts +++ b/index.d.ts @@ -21,7 +21,7 @@ export class SingularConfig { withClipboardAttribution(): SingularConfig; withManualSkanConversionManagement(): SingularConfig; withConversionValueUpdatedHandler(handler: (value: number) => void): SingularConfig; - withConversionValuesUpdatedHandler(handler: (fineValue: number, coarseValue: number, lockWindow: boolean) => void): SingularConfig; + withConversionValuesUpdatedHandler(handler: (updatedValues: SerializableObject) => void): SingularConfig; withWaitForTrackingAuthorizationWithTimeoutInterval(interval: number): SingularConfig; withShortLinkResolveTimeout(shortLinkResolveTimeout: number): SingularConfig; withLimitDataSharing(shouldLimitDataSharing: boolean): SingularConfig; @@ -31,7 +31,7 @@ export class SingularConfig { withLogLevel(level: number): SingularConfig; withEspDomains(domains: [string]) : SingularConfig; withFacebookAppId(appId: string): SingularConfig; - withDeviceAttributionCallbackHandler(deviceAttributionCallbackHandler:(attributes: Map) => void): SingularConfig; + withDeviceAttributionCallbackHandler(deviceAttributionCallbackHandler:(attributes: SerializableObject) => void): SingularConfig; withCustomSdid(customSdid: string, didSetSdidCallback: (result: string) => void, sdidReceivedCallback: (result: string) => void): SingularConfig; withPushNotificationsLinkPaths(pushNotificationsLinkPaths: [[string]]) : SingularConfig; withBrandedDomains(domains: [string]) : SingularConfig; @@ -68,8 +68,8 @@ export class Singular { static customRevenue(eventName: string, currency: string, amount: number): void; static customRevenueWithArgs(eventName: string, currency: string, amount: number, args: SerializableObject): void; - static inAppPurchase(eventName: string, purchase: SingularPurchase): void; - static inAppPurchaseWithArgs(eventName: string, purchase: SingularPurchase, args: SerializableObject): void; + static inAppPurchase(eventName: string, purchase: SingularPurchase | any): void; + static inAppPurchaseWithArgs(eventName: string, purchase: SingularPurchase | any, args: SerializableObject): void; static setUninstallToken(token: string): void; @@ -79,7 +79,7 @@ export class Singular { static resumeAllTracking(): void; static isAllTrackingStopped(): boolean; - static limitDataSharing(shoudlLimitDataSharing: boolean): void; + static limitDataSharing(shouldLimitDataSharing: boolean): void; static getLimitDataSharing(): boolean; static setGlobalProperty(key: string, value: string, overrideExisting: boolean): boolean; @@ -94,14 +94,9 @@ export class Singular { static createReferrerShortLink(baseLink: string, referrerName: string, referrerId: string, passthroughParams: SerializableObject, completionHandler: (result: string, error: string) => void): void; - static adRevenue(adData: SingularAdData): void; - - static setGlobalProperty(key: string, value: string, overrideExisting: boolean): boolean; - static unsetGlobalProperty(key: string): void; - static clearGlobalProperties(): void; - static getGlobalProperties(): Map; + static adRevenue(adData: SingularAdData | any): void; - static handlePushNotification(pushNotificationPayload: boolean): void; + static handlePushNotification(pushNotificationPayload: SerializableObject): void; static setLimitAdvertisingIdentifiers(enabled: boolean): void; } diff --git a/index.js b/index.js index 64156bf..966f310 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,6 @@ -export {Singular} from "./Singular" -export {SingularConfig} from "./SingularConfig"; -export {Events} from "./Events"; -export {Attributes} from "./Attributes"; -export {SingularPurchase, SingularIOSPurchase, SingularAndroidPurchase} from "./SingularPurchase" -export {SingularAdData} from "./SingularAdData"; - +export { Singular, SingularBridge } from './Singular'; +export { SingularConfig } from "./SingularConfig"; +export { Events } from "./Events"; +export { Attributes } from "./Attributes"; +export { SingularPurchase, SingularIOSPurchase, SingularAndroidPurchase } from "./SingularPurchase" +export { SingularAdData } from "./SingularAdData"; diff --git a/ios/SingularBridge.h b/ios/SingularBridge.h index d74846c..63f9b9c 100644 --- a/ios/SingularBridge.h +++ b/ios/SingularBridge.h @@ -2,14 +2,22 @@ #import #import #else -#import “RCTBridgeModule.h” -#import “RCTEventEmitter.h” +#import "RCTBridgeModule.h" +#import "RCTEventEmitter.h" #endif -@interface SingularBridge : RCTEventEmitter { -} +#if RCT_NEW_ARCH_ENABLED +#import +#import +#endif + +#if RCT_NEW_ARCH_ENABLED +@interface SingularBridge : RCTEventEmitter +#else +@interface SingularBridge : RCTEventEmitter +#endif -+(void)startSessionWithLaunchOptions:(NSDictionary*)options; -+(void)startSessionWithUserActivity:(NSUserActivity*)userActivity; ++ (void)startSessionWithUserActivity:(NSUserActivity*)userActivity; ++ (void)startSessionWithLaunchOptions:(NSDictionary*)options; @end diff --git a/ios/SingularBridgeNewArch.mm b/ios/SingularBridgeNewArch.mm new file mode 100644 index 0000000..d8d8932 --- /dev/null +++ b/ios/SingularBridgeNewArch.mm @@ -0,0 +1,445 @@ +#import "SingularBridge.h" +#import "SingularHelper.h" + +#import +#import +#import + +#if RCT_NEW_ARCH_ENABLED +#import "NativeSingular.h" +#import "NativeSingularJSI.h" +#import +#endif + +@implementation SingularBridge + +static RCTEventEmitter* eventEmitter; + +static NSString *apikey; +static NSString *secret; +static NSDictionary *launchOptions; + +static NSString* const version = @"4.0.0"; +static NSString* const wrapper = @"ReactNative"; + +// Ad Revenue key constants +static NSString* const kAdRevenueEvent = @"__ADMON_USER_LEVEL_REVENUE__"; +static NSString* const kAdPlatform = @"ad_platform"; +static NSString* const kAdCurrency = @"ad_currency"; +static NSString* const kAdRevenue = @"ad_revenue"; +static NSString* const kAdRevenueR = @"r"; +static NSString* const kAdRevenuePCC = @"pcc"; +static NSString* const kIsAdmonRevenue = @"is_admon_revenue"; +static NSString* const kIsRevenueEvent = @"is_revenue_event"; +static NSString* const kAdMediationPlatform = @"ad_mediation_platform"; +static NSString* const kAdType = @"ad_type"; +static NSString* const kAdGroupType = @"ad_group_type"; +static NSString* const kAdImpressionId = @"ad_impression_id"; +static NSString* const kAdPlacementName = @"ad_placement_name"; +static NSString* const kAdUnitId = @"ad_unit_id"; +static NSString* const kAdUnitName = @"ad_unit_name"; +static NSString* const kAdGroupId = @"ad_group_id"; +static NSString* const kAdGroupName = @"ad_group_name"; +static NSString* const kAdGroupPriority = @"ad_group_priority"; +static NSString* const kAdPrecision = @"ad_precision"; +static NSString* const kAdPlacementId = @"ad_placement_id"; + +// Purchase key constants +static NSString* const kPurchaseProductKey = @"pk"; +static NSString* const kPurchaseTransactionId = @"pti"; +static NSString* const kPurchaseReceipt = @"ptr"; + +// Singular Link key constants +static NSString* const kSingularLinkDeeplink = @"deeplink"; +static NSString* const kSingularLinkPassthrough = @"passthrough"; +static NSString* const kSingularLinkIsDeferred = @"isDeferred"; +static NSString* const kSingularLinkUrlParameters = @"urlParameters"; + +RCT_EXPORT_MODULE(SingularBridge); + ++(void)startSessionWithLaunchOptions:(NSDictionary*)options { + launchOptions = options; +} + +// Handling Singular Link when the app is opened from a Singular Link while it was in the background. +// The client will need to call this method in the AppDelegate in continueUserActivity. ++(void)startSessionWithUserActivity:(NSUserActivity*)userActivity { + [Singular startSession:apikey + withKey:secret + andUserActivity:userActivity + withSingularLinkHandler:^(SingularLinkParams * params) { + NSDictionary *linkData = @{ + kSingularLinkDeeplink: [params getDeepLink] ? [params getDeepLink] : @"", + kSingularLinkPassthrough: [params getPassthrough] ? [params getPassthrough] : @"", + kSingularLinkIsDeferred: [params isDeferred] ? @YES : @NO, + kSingularLinkUrlParameters: [params getUrlParameters] ? [params getUrlParameters] : @{} + }; + + [eventEmitter sendEventWithName:SINGULAR_LINK_HANDLER_CONST body:linkData]; + }]; +} + +- (NSArray *)supportedEvents { + return [SingularHelper supportedEvents]; +} + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +#pragma mark - SingularSpec Protocol + +- (void)init:(JS::NativeSingular::SingularConfig &)config { + SingularConfig *singularConfig = [[SingularConfig alloc] initWithApiKey:config.apikey() andSecret:config.secret()]; + singularConfig.launchOptions = launchOptions; + + apikey = config.apikey(); + secret = config.secret(); + + if (config.skAdNetworkEnabled().has_value()) { + singularConfig.skAdNetworkEnabled = config.skAdNetworkEnabled().value(); + } + if (config.clipboardAttribution().has_value()) { + singularConfig.clipboardAttribution = config.clipboardAttribution().value(); + } + if (config.shortLinkResolveTimeout().has_value()) { + singularConfig.shortLinkResolveTimeOut = config.shortLinkResolveTimeout().value(); + } + if (config.manualSkanConversionManagement().has_value()) { + singularConfig.manualSkanConversionManagement = config.manualSkanConversionManagement().value(); + } + if (config.waitForTrackingAuthorizationWithTimeoutInterval().has_value()) { + singularConfig.waitForTrackingAuthorizationWithTimeoutInterval = config.waitForTrackingAuthorizationWithTimeoutInterval().value(); + } + if (config.customSdid()) { + singularConfig.customSdid = config.customSdid(); + } + if (config.limitAdvertisingIdentifiers().has_value()) { + singularConfig.limitAdvertisingIdentifiers = config.limitAdvertisingIdentifiers().value(); + } + if (config.enableOdmWithTimeoutInterval().has_value()) { + singularConfig.enableOdmWithTimeoutInterval = config.enableOdmWithTimeoutInterval().value(); + } + if (config.sessionTimeout().has_value()) { + [SingularHelper setSessionTimeout:(int)config.sessionTimeout().value()]; + } + if (config.limitDataSharing().has_value()) { + [SingularHelper limitDataSharing:config.limitDataSharing().value()]; + } + if (config.customUserId()) { + [SingularHelper setCustomUserId:config.customUserId()]; + } + + if (config.espDomains().has_value()) { + auto espDomains = config.espDomains().value(); + NSMutableArray *espDomainsArray = [[NSMutableArray alloc] init]; + for (const auto& domain : espDomains) { + [espDomainsArray addObject:domain]; + } + singularConfig.espDomains = [espDomainsArray copy]; + } + + if (config.brandedDomains().has_value()) { + auto brandedDomains = config.brandedDomains().value(); + NSMutableArray *brandedDomainsArray = [[NSMutableArray alloc] init]; + for (const auto& domain : brandedDomains) { + [brandedDomainsArray addObject:domain]; + } + singularConfig.brandedDomains = [brandedDomainsArray copy]; + } + + if (config.pushNotificationsLinkPaths().has_value()) { + auto pushPaths = config.pushNotificationsLinkPaths().value(); + NSMutableArray *> *pushPathsArray = [[NSMutableArray alloc] init]; + for (const auto& pathGroup : pushPaths) { + NSMutableArray *pathGroupArray = [[NSMutableArray alloc] init]; + for (const auto& path : pathGroup) { + [pathGroupArray addObject:path]; + } + [pushPathsArray addObject:[pathGroupArray copy]]; + } + singularConfig.pushNotificationLinkPath = [pushPathsArray copy]; + } + + if (config.globalProperties()) { + NSDictionary *globalProps = (NSDictionary *)config.globalProperties(); + if (globalProps && [globalProps count] > 0) { + for (NSDictionary *property in [globalProps allValues]) { + if ([property isKindOfClass:[NSDictionary class]]) { + NSString *propertyKey = [property objectForKey:@"Key"]; + NSString *propertyValue = [property objectForKey:@"Value"]; + BOOL overrideExisting = [[property objectForKey:@"OverrideExisting"] boolValue]; + + if (propertyKey && propertyValue) { + [singularConfig setGlobalProperty:propertyKey + withValue:propertyValue + overrideExisting:overrideExisting]; + } + } + } + } + } + + singularConfig.singularLinksHandler = ^(SingularLinkParams * _Nonnull params) { + NSDictionary *linkData = @{ + kSingularLinkDeeplink: [params getDeepLink] ?: @"", + kSingularLinkPassthrough: [params getPassthrough] ?: @"", + kSingularLinkIsDeferred: @([params isDeferred]), + kSingularLinkUrlParameters: [params getUrlParameters] ?: @{} + }; + + [self sendEventWithName:SINGULAR_LINK_HANDLER_CONST body:linkData]; + }; + + singularConfig.sdidReceivedHandler = ^(NSString * _Nonnull sdid) { + [eventEmitter sendEventWithName:SDID_RECEIVED_CALLBACK_CONST body:sdid]; + }; + + singularConfig.didSetSdidHandler = ^(NSString * _Nonnull sdid) { + [eventEmitter sendEventWithName:SDID_SET_CALLBACK_CONST body:sdid]; + }; + + singularConfig.conversionValueUpdatedCallback = ^(NSInteger value) { + [eventEmitter sendEventWithName:CONVERSION_VALUE_UPDATED_HANDLER_CONST body:@(value)]; + }; + + singularConfig.conversionValuesUpdatedCallback = ^(NSNumber *fineValue, NSNumber *coarseValue, BOOL lockWindow) { + NSInteger fine = -1; + NSInteger coarse = -1; + + if (fineValue != nil) { + fine = [fineValue intValue]; + } + if (coarseValue != nil) { + coarse = [coarseValue intValue]; + } + + NSDictionary *conversionValues = @{ + @"conversionValue": @(fine), + @"coarse": @(coarse), + @"lock": @(lockWindow) + }; + + [eventEmitter sendEventWithName:CONVERSION_VALUES_UPDATED_HANDLER_CONST body:conversionValues]; + }; + + singularConfig.deviceAttributionCallback = ^(NSDictionary *attributionData) { + [eventEmitter sendEventWithName:DEVICE_ATTRIBUTION_CALLBACK_HANDLER_CONST body:attributionData]; + }; + + eventEmitter = self; + + [SingularHelper initWithConfig:singularConfig]; + [SingularHelper setReactSDKVersion:wrapper version:version]; +} + +#pragma mark - Event Methods + +- (void)event:(NSString *)eventName { + [SingularHelper event:eventName]; +} + +- (void)eventWithArgs:(NSString *)eventName args:(NSDictionary *)args { + [SingularHelper eventWithArgs:eventName args:args]; +} + +- (void)adRevenue:(JS::NativeSingular::SingularAdData &)adData { + NSMutableDictionary *adRevenueData = [NSMutableDictionary dictionary]; + adRevenueData[kAdPlatform] = adData.ad_platform(); + adRevenueData[kAdCurrency] = adData.ad_currency(); + adRevenueData[kAdRevenue] = @(adData.ad_revenue()); + adRevenueData[kAdRevenueR] = @(adData.ad_revenue()); + adRevenueData[kAdRevenuePCC] = adData.ad_currency(); + adRevenueData[kIsAdmonRevenue] = @(YES); + adRevenueData[kIsRevenueEvent] = @(YES); + + if (adData.ad_mediation_platform()) { + adRevenueData[kAdMediationPlatform] = adData.ad_mediation_platform(); + } + if (adData.ad_type()) { + adRevenueData[kAdType] = adData.ad_type(); + } + if (adData.ad_group_type()) { + adRevenueData[kAdGroupType] = adData.ad_group_type(); + } + if (adData.ad_impression_id()) { + adRevenueData[kAdImpressionId] = adData.ad_impression_id(); + } + if (adData.ad_placement_name()) { + adRevenueData[kAdPlacementName] = adData.ad_placement_name(); + } + if (adData.ad_unit_id()) { + adRevenueData[kAdUnitId] = adData.ad_unit_id(); + } + if (adData.ad_unit_name()) { + adRevenueData[kAdUnitName] = adData.ad_unit_name(); + } + if (adData.ad_group_id()) { + adRevenueData[kAdGroupId] = adData.ad_group_id(); + } + if (adData.ad_group_name()) { + adRevenueData[kAdGroupName] = adData.ad_group_name(); + } + if (adData.ad_group_priority().has_value()) { + adRevenueData[kAdGroupPriority] = @(adData.ad_group_priority().value()); + } + if (adData.ad_precision()) { + adRevenueData[kAdPrecision] = adData.ad_precision(); + } + if (adData.ad_placement_id()) { + adRevenueData[kAdPlacementId] = adData.ad_placement_id(); + } + + [SingularHelper eventWithArgs:kAdRevenueEvent args:adRevenueData]; +} + +- (NSDictionary *)getPurchaseValues:(JS::NativeSingular::SingularPurchase &)purchase { + NSMutableDictionary *purchaseValues = [NSMutableDictionary dictionary]; + purchaseValues[kAdRevenueR] = @(purchase.revenue()); + purchaseValues[kAdRevenuePCC] = purchase.currency(); + purchaseValues[kIsRevenueEvent] = @(YES); + + if (purchase.productId()) { + purchaseValues[kPurchaseProductKey] = purchase.productId(); + } + if (purchase.transactionId()) { + purchaseValues[kPurchaseTransactionId] = purchase.transactionId(); + } + if (purchase.receipt()) { + purchaseValues[kPurchaseReceipt] = purchase.receipt(); + } + + return purchaseValues; +} + +- (void)inAppPurchase:(NSString *)eventName purchase:(JS::NativeSingular::SingularPurchase &)purchase { + NSDictionary *purchaseValues = [self getPurchaseValues:purchase]; + [SingularHelper eventWithArgs:eventName args:purchaseValues]; +} + +- (void)inAppPurchaseWithArgs:(NSString *)eventName purchase:(JS::NativeSingular::SingularPurchase &)purchase args:(NSDictionary *)args { + NSDictionary *purchaseValues = [self getPurchaseValues:purchase]; + NSMutableDictionary *combinedArgs = [purchaseValues mutableCopy]; + [combinedArgs addEntriesFromDictionary:args]; + + [SingularHelper eventWithArgs:eventName args:combinedArgs]; +} + +- (void)clearGlobalProperties { + [SingularHelper clearGlobalProperties]; +} + +- (void)createReferrerShortLink:(NSString *)baseLink referrerName:(NSString *)referrerName referrerId:(NSString *)referrerId passthroughParams:(NSDictionary *)passthroughParams completionHandler:(RCTResponseSenderBlock)completionHandler { + [SingularHelper createReferrerShortLink:baseLink referrerName:referrerName referrerId:referrerId passthroughParams:passthroughParams completionHandler:^(NSString *result, NSString *error) { + if (result) { + completionHandler(@[result, @""]); + } else { + completionHandler(@[@"", error]); + } + }]; +} + +- (void)customRevenue:(NSString *)eventName currency:(NSString *)currency amount:(double)amount { + [SingularHelper customRevenue:eventName currency:currency amount:amount]; +} + +- (void)customRevenueWithArgs:(NSString *)eventName currency:(NSString *)currency amount:(double)amount args:(NSDictionary *)args { + [SingularHelper customRevenueWithArgs:eventName currency:currency amount:amount args:args]; +} + +- (NSDictionary *)getGlobalProperties { + return [SingularHelper getGlobalProperties]; +} + +- (NSNumber *)getLimitDataSharing { + BOOL isLimited = [SingularHelper getLimitDataSharing]; + return @(isLimited); +} + +- (void)handlePushNotification:(NSDictionary *)pushNotificationPayload { + [SingularHelper handlePushNotification:pushNotificationPayload]; +} + +- (NSNumber *)isAllTrackingStopped { + BOOL isStopped = [SingularHelper isAllTrackingStopped]; + return @(isStopped); +} + +- (void)limitDataSharing:(BOOL)shouldLimitDataSharing { + [SingularHelper limitDataSharing:shouldLimitDataSharing]; +} + +- (void)resumeAllTracking { + [SingularHelper resumeAllTracking]; +} + +- (void)revenue:(NSString *)currency amount:(double)amount { + [SingularHelper revenue:currency amount:amount]; +} + +- (void)revenueWithArgs:(NSString *)currency amount:(double)amount args:(NSDictionary *)args { + [SingularHelper revenueWithArgs:currency amount:amount args:args]; +} + +- (void)setCustomUserId:(NSString *)customUserId { + [SingularHelper setCustomUserId:customUserId]; +} + +- (void)setDeviceCustomUserId:(NSString *)customUserId { + [SingularHelper setDeviceCustomUserId:customUserId]; +} + +- (NSNumber *)setGlobalProperty:(NSString *)key value:(NSString *)value overrideExisting:(BOOL)overrideExisting { + BOOL success = [SingularHelper setGlobalProperty:key value:value overrideExisting:overrideExisting]; + return @(success); +} + +- (void)setLimitAdvertisingIdentifiers:(BOOL)enabled { + [SingularHelper setLimitAdvertisingIdentifiers:enabled]; +} + +- (void)setUninstallToken:(NSString *)token { + [SingularHelper setUninstallToken:token]; +} + +- (NSNumber *)skanGetConversionValue { + return [SingularHelper skanGetConversionValue]; +} + +- (void)skanRegisterAppForAdNetworkAttribution { + [SingularHelper skanRegisterAppForAdNetworkAttribution]; +} + +- (NSNumber *)skanUpdateConversionValue:(double)conversionValue { + BOOL success = [SingularHelper skanUpdateConversionValue:(NSInteger)conversionValue]; + return @(success); +} + +- (void)skanUpdateConversionValues:(double)conversionValue coarse:(double)coarse lock:(BOOL)lock { + [SingularHelper skanUpdateConversionValues:(NSInteger)conversionValue coarse:(NSInteger)coarse lock:lock]; +} + +- (void)stopAllTracking { + [SingularHelper stopAllTracking]; +} + +- (void)trackingOptIn { + [SingularHelper trackingOptIn]; +} + +- (void)trackingUnder13 { + [SingularHelper trackingUnder13]; +} + +- (void)unsetCustomUserId { + [SingularHelper unsetCustomUserId]; +} + +- (void)unsetGlobalProperty:(NSString *)key { + [SingularHelper unsetGlobalProperty:key]; +} + +@end + diff --git a/ios/SingularBridge.m b/ios/SingularBridgeOldArch.m similarity index 68% rename from ios/SingularBridge.m rename to ios/SingularBridgeOldArch.m index 5be2def..ff304a5 100644 --- a/ios/SingularBridge.m +++ b/ios/SingularBridgeOldArch.m @@ -17,13 +17,7 @@ #import “RCTEventDispatcher.h” #endif -#define SINGULAR_LINK_HANDLER_CONST @"SingularLinkHandler" -#define CONVERSION_VALUE_UPDATED_HANDLER_CONST @"ConversionValueUpdatedHandler" -#define DEVICE_ATTRIBUTION_CALLBACK_HANDLER_CONST @"DeviceAttributionCallbackHandler" -#define CONVERSION_VALUES_UPDATED_HANDLER_CONST @"ConversionValuesUpdatedHandler" -#define SHORT_LINK_HANDLER_CONST @"ShortLinkHandler" -#define SDID_RECEIVED_CALLBACK_CONST @"SdidReceivedCallback" -#define SDID_SET_CALLBACK_CONST @"DidSetSdidCallback" +#import "SingularHelper.h" @implementation SingularBridge @synthesize bridge = _bridge; @@ -46,24 +40,18 @@ +(void)startSessionWithUserActivity:(NSUserActivity*)userActivity{ withKey:secret andUserActivity:userActivity withSingularLinkHandler:^(SingularLinkParams * params){ - [SingularBridge handleSingularLink:params]; - }]; + [SingularBridge handleSingularLink:params]; + }]; } RCT_EXPORT_MODULE(); - (NSArray *)supportedEvents { - return @[SINGULAR_LINK_HANDLER_CONST, - CONVERSION_VALUE_UPDATED_HANDLER_CONST, - SHORT_LINK_HANDLER_CONST, - CONVERSION_VALUES_UPDATED_HANDLER_CONST, - DEVICE_ATTRIBUTION_CALLBACK_HANDLER_CONST, - SDID_RECEIVED_CALLBACK_CONST, - SDID_SET_CALLBACK_CONST]; + return [SingularHelper supportedEvents]; } // Init method using a json string representing the config -RCT_EXPORT_METHOD(init:(NSString*) jsonSingularConfig){ +RCT_EXPORT_METHOD(init:(NSString*) jsonSingularConfig) { NSDictionary* singularConfigDict = [SingularBridge jsonToDictionary:jsonSingularConfig]; apikey = [singularConfigDict objectForKey:@"apikey"]; @@ -85,10 +73,10 @@ +(void)startSessionWithUserActivity:(NSUserActivity*)userActivity{ // Global Properties fields NSDictionary* globalProperties = [singularConfigDict objectForKey:@"globalProperties"]; if (globalProperties && [globalProperties count] > 0){ - for (NSDictionary* property in [globalProperties allValues]) { - [singularConfig setGlobalProperty:[property objectForKey:@"Key"] - withValue:[property objectForKey:@"Value"] - overrideExisting:[[property objectForKey:@"OverrideExisting"] boolValue]]; + for (NSDictionary* property in [globalProperties allValues]) { + [singularConfig setGlobalProperty:[property objectForKey:@"Key"] + withValue:[property objectForKey:@"Value"] + overrideExisting:[[property objectForKey:@"OverrideExisting"] boolValue]]; } } @@ -102,9 +90,9 @@ +(void)startSessionWithUserActivity:(NSUserActivity*)userActivity{ singularConfig.conversionValuesUpdatedCallback = ^(NSNumber *fineValue, NSNumber *coarseValue, BOOL lockWindow) { [SingularBridge handleConversionValuesUpdated:fineValue andCoarseValue:coarseValue andLockWindow:lockWindow]; }; - + singularConfig.waitForTrackingAuthorizationWithTimeoutInterval = - [[singularConfigDict objectForKey:@"waitForTrackingAuthorizationWithTimeoutInterval"] intValue]; + [[singularConfigDict objectForKey:@"waitForTrackingAuthorizationWithTimeoutInterval"] intValue]; singularConfig.enableOdmWithTimeoutInterval = [[singularConfigDict objectForKey:@"enableOdmWithTimeoutInterval"] intValue]; @@ -112,30 +100,30 @@ +(void)startSessionWithUserActivity:(NSUserActivity*)userActivity{ singularConfig.deviceAttributionCallback = ^(NSDictionary *deviceAttributionData) { [SingularBridge handleDeviceAttributionData:deviceAttributionData]; }; - + NSString* customUserId = [singularConfigDict objectForKey:@"customUserId"]; if (customUserId) { - [Singular setCustomUserId:customUserId]; + [SingularHelper setCustomUserId:customUserId]; } NSNumber* limitDataSharing = [singularConfigDict objectForKey:@"limitDataSharing"]; if (![limitDataSharing isEqual:[NSNull null]]) { - [Singular limitDataSharing:[limitDataSharing boolValue]]; + [SingularHelper limitDataSharing:[limitDataSharing boolValue]]; } NSNumber* sessionTimeout = [singularConfigDict objectForKey:@"sessionTimeout"]; if ([sessionTimeout intValue] >= 0) { - [Singular setSessionTimeout:[sessionTimeout intValue]]; + [SingularHelper setSessionTimeout:[sessionTimeout intValue]]; } NSString *customSdid = [singularConfigDict objectForKey:@"customSdid"]; if (![SingularBridge isValidNonEmptyString:customSdid]) { customSdid = nil; } - + singularConfig.customSdid = customSdid; singularConfig.sdidReceivedHandler = ^(NSString *result) { @@ -145,7 +133,7 @@ +(void)startSessionWithUserActivity:(NSUserActivity*)userActivity{ singularConfig.didSetSdidHandler = ^(NSString *result) { [eventEmitter sendEventWithName:SDID_SET_CALLBACK_CONST body:result]; }; - + // push singularConfig.pushNotificationLinkPath = [singularConfigDict objectForKey:@"pushNotificationsLinkPaths"]; @@ -153,43 +141,43 @@ +(void)startSessionWithUserActivity:(NSUserActivity*)userActivity{ eventEmitter = self; - [Singular start:singularConfig]; + [SingularHelper initWithConfig:singularConfig]; } RCT_EXPORT_METHOD(createReferrerShortLink:(NSString *)baseLink - referrerName:(NSString *)referrerName - referrerId:(NSString *)referrerId - passthroughParams:(NSString *)args){ - [Singular createReferrerShortLink:baseLink + referrerName:(NSString *)referrerName + referrerId:(NSString *)referrerId + passthroughParams:(NSString *)args){ + [SingularHelper createReferrerShortLink:baseLink referrerName:referrerName referrerId:referrerId passthroughParams:[SingularBridge jsonToDictionary:args] - completionHandler:^(NSString *data, NSError *error) { - [eventEmitter sendEventWithName:SHORT_LINK_HANDLER_CONST body:@{ - @"data": data? data: @"", - @"error": error ? error.description: @"" - }]; - }]; + completionHandler:^(NSString *data, NSString *error) { + [eventEmitter sendEventWithName:SHORT_LINK_HANDLER_CONST body:@{ + @"data": data ? data: @"", + @"error": error ? error: @"" + }]; + }]; } RCT_EXPORT_METHOD(setCustomUserId:(NSString*)customUserId){ - [Singular setCustomUserId:customUserId]; + [SingularHelper setCustomUserId:customUserId]; } RCT_EXPORT_METHOD(unsetCustomUserId){ - [Singular unsetCustomUserId]; + [SingularHelper unsetCustomUserId]; } RCT_EXPORT_METHOD(setDeviceCustomUserId:(NSString*)customUserId){ - [Singular setDeviceCustomUserId:customUserId]; + [SingularHelper setDeviceCustomUserId:customUserId]; } RCT_EXPORT_METHOD(event:(NSString*)eventName){ - [Singular event:eventName]; + [SingularHelper event:eventName]; } RCT_EXPORT_METHOD(eventWithArgs:(NSString*)eventName args:(NSString*)args){ - [Singular event:eventName withArgs:[SingularBridge jsonToDictionary:args]]; + [SingularHelper eventWithArgs:eventName args:[SingularBridge jsonToDictionary:args]]; } RCT_EXPORT_METHOD(revenue:(NSString*)currency amount:(double)amount){ @@ -201,91 +189,91 @@ +(void)startSessionWithUserActivity:(NSUserActivity*)userActivity{ } RCT_EXPORT_METHOD(customRevenue:(NSString*)eventName currency:(NSString*)currency amount:(double)amount){ - [Singular customRevenue:eventName currency:currency amount:amount]; + [SingularHelper customRevenue:eventName currency:currency amount:amount]; } RCT_EXPORT_METHOD(customRevenueWithArgs:(NSString*)eventName currency:(NSString*)currency amount:(double)amount args:(NSString*)args){ - [Singular customRevenue:eventName currency:currency amount:amount withAttributes:[SingularBridge jsonToDictionary:args]]; + [SingularHelper customRevenueWithArgs:eventName currency:currency amount:amount args:[SingularBridge jsonToDictionary:args]]; } RCT_EXPORT_METHOD(setUninstallToken:(NSString*)token){ NSData *tokenData = [SingularBridge convertHexStringToDataBytes:token]; if (tokenData) { - [Singular registerDeviceTokenForUninstall:tokenData]; + [SingularHelper setUninstallToken:token]; } } RCT_EXPORT_METHOD(trackingOptIn){ - [Singular trackingOptIn]; + [SingularHelper trackingOptIn]; } RCT_EXPORT_METHOD(trackingUnder13){ - [Singular trackingUnder13]; + [SingularHelper trackingUnder13]; } RCT_EXPORT_METHOD(stopAllTracking){ - [Singular stopAllTracking]; + [SingularHelper stopAllTracking]; } RCT_EXPORT_METHOD(resumeAllTracking){ - [Singular resumeAllTracking]; + [SingularHelper resumeAllTracking]; } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(isAllTrackingStopped){ - return [Singular isAllTrackingStopped] ? @YES : @NO; + return [SingularHelper isAllTrackingStopped] ? @YES : @NO; } RCT_EXPORT_METHOD(limitDataSharing:(BOOL)limitDataSharingValue){ - [Singular limitDataSharing:limitDataSharingValue]; + [SingularHelper limitDataSharing:limitDataSharingValue]; } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getLimitDataSharing){ - return [Singular getLimitDataSharing] ? @YES : @NO; + return [SingularHelper getLimitDataSharing] ? @YES : @NO; } RCT_EXPORT_METHOD(setReactSDKVersion:(NSString*)wrapper version:(NSString*)version){ - [Singular setWrapperName:wrapper andVersion:version]; + [SingularHelper setReactSDKVersion:wrapper version:version]; } // export SKAN methods RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(skanUpdateConversionValue:(NSInteger)conversionValue){ - return [Singular skanUpdateConversionValue:conversionValue] ? @YES : @NO; + return [SingularHelper skanUpdateConversionValue:conversionValue] ? @YES : @NO; } RCT_EXPORT_METHOD(skanUpdateConversionValues:(NSInteger)conversionValue coarse:(NSInteger)coarse lock:(BOOL)lock){ - [Singular skanUpdateConversionValue:conversionValue coarse:coarse lock:lock]; + [SingularHelper skanUpdateConversionValues:conversionValue coarse:coarse lock:lock]; } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(skanGetConversionValue){ - return [Singular skanGetConversionValue]; + return [SingularHelper skanGetConversionValue]; } RCT_EXPORT_METHOD(skanRegisterAppForAdNetworkAttribution){ - [Singular skanRegisterAppForAdNetworkAttribution]; + [SingularHelper skanRegisterAppForAdNetworkAttribution]; } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(setGlobalProperty:(NSString *)key value:(NSString *)value overrideExisting:(BOOL)overrideExisting) { - return [Singular setGlobalProperty:key andValue:value overrideExisting:overrideExisting] ? @YES : @NO; + return [SingularHelper setGlobalProperty:key value:value overrideExisting:overrideExisting] ? @YES : @NO; } RCT_EXPORT_METHOD(unsetGlobalProperty:(NSString *) key) { - [Singular unsetGlobalProperty:key]; + [SingularHelper unsetGlobalProperty:key]; } RCT_EXPORT_METHOD(clearGlobalProperties) { - [Singular clearGlobalProperties]; + [SingularHelper clearGlobalProperties]; } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getGlobalProperties) { - return [Singular getGlobalProperties]; + return [SingularHelper getGlobalProperties]; } RCT_EXPORT_METHOD(handlePushNotification:(NSDictionary *)pushNotificationPayload) { - [Singular handlePushNotification:pushNotificationPayload]; + [SingularHelper handlePushNotification:pushNotificationPayload]; } RCT_EXPORT_METHOD(setLimitAdvertisingIdentifiers:(BOOL)enabled) { - [Singular setLimitAdvertisingIdentifiers:enabled]; + [SingularHelper setLimitAdvertisingIdentifiers:enabled]; } #pragma mark - Private methods @@ -311,10 +299,10 @@ +(NSDictionary*)jsonToDictionary:(NSString*)json{ +(void)handleSingularLink:(SingularLinkParams*)params { // Raising the Singular Link handler in the react-native code [eventEmitter sendEventWithName:SINGULAR_LINK_HANDLER_CONST body:@{ - @"deeplink": [params getDeepLink] ? [params getDeepLink] : @"", - @"passthrough": [params getPassthrough] ? [params getPassthrough] : @"", - @"isDeferred": [params isDeferred] ? @YES : @NO, - @"urlParameters": [params getUrlParameters] ? [params getUrlParameters] : @{ } + @"deeplink": [params getDeepLink] ? [params getDeepLink] : @"", + @"passthrough": [params getPassthrough] ? [params getPassthrough] : @"", + @"isDeferred": [params isDeferred] ? @YES : @NO, + @"urlParameters": [params getUrlParameters] ? [params getUrlParameters] : @{ } }]; } @@ -332,7 +320,7 @@ +(void)handleDeviceAttributionData:(NSDictionary *)attributionData { +(void)handleConversionValuesUpdated:(NSNumber *)fineValue andCoarseValue:(NSNumber *)coarseValue andLockWindow:(BOOL)lockWindow { NSInteger fine = -1; NSInteger coarse = -1; - + if (fineValue != nil) { fine = [fineValue intValue]; } @@ -341,9 +329,9 @@ +(void)handleConversionValuesUpdated:(NSNumber *)fineValue andCoarseValue:(NSNum } [eventEmitter sendEventWithName:CONVERSION_VALUES_UPDATED_HANDLER_CONST body:@{ - @"conversionValue": @(fine), - @"coarse": @(coarse), - @"lock": @(lockWindow) + @"conversionValue": @(fine), + @"coarse": @(coarse), + @"lock": @(lockWindow) }]; } @@ -365,19 +353,19 @@ + (NSData *)convertHexStringToDataBytes:(NSString *)hexString { wholeByte = strtoul(byteChars, NULL, 16); [data appendBytes:&wholeByte length:1]; } - + return data; } + (BOOL)isValidNonEmptyString:(NSString *)nullableJavascriptString { return nullableJavascriptString != nil - && ![nullableJavascriptString isEqual:[NSNull null]] - && [nullableJavascriptString isKindOfClass:NSString.class] - && nullableJavascriptString.length > 0 - && ![nullableJavascriptString.lowercaseString isEqualToString:@"null"] - && ![nullableJavascriptString.lowercaseString isEqualToString:@"undefined"] - && ![nullableJavascriptString.lowercaseString isEqualToString:@"false"] - && ![nullableJavascriptString isEqualToString:@"NaN"]; + && ![nullableJavascriptString isEqual:[NSNull null]] + && [nullableJavascriptString isKindOfClass:NSString.class] + && nullableJavascriptString.length > 0 + && ![nullableJavascriptString.lowercaseString isEqualToString:@"null"] + && ![nullableJavascriptString.lowercaseString isEqualToString:@"undefined"] + && ![nullableJavascriptString.lowercaseString isEqualToString:@"false"] + && ![nullableJavascriptString isEqualToString:@"NaN"]; } @end diff --git a/ios/SingularHelper.h b/ios/SingularHelper.h new file mode 100644 index 0000000..fe8895d --- /dev/null +++ b/ios/SingularHelper.h @@ -0,0 +1,70 @@ +#import + +@class SingularConfig; + +#define SINGULAR_LINK_HANDLER_CONST @"SingularLinkHandler" +#define CONVERSION_VALUE_UPDATED_HANDLER_CONST @"ConversionValueUpdatedHandler" +#define DEVICE_ATTRIBUTION_CALLBACK_HANDLER_CONST @"DeviceAttributionCallbackHandler" +#define CONVERSION_VALUES_UPDATED_HANDLER_CONST @"ConversionValuesUpdatedHandler" +#define SHORT_LINK_HANDLER_CONST @"ShortLinkHandler" +#define SDID_RECEIVED_CALLBACK_CONST @"SdidReceivedCallback" +#define SDID_SET_CALLBACK_CONST @"DidSetSdidCallback" + +@interface SingularHelper : NSObject + ++ (void)initWithConfig:(SingularConfig *)config; + ++ (void)setSessionTimeout:(int)sessionTimeout; ++ (void)event:(NSString *)eventName; ++ (void)eventWithArgs:(NSString *)eventName args:(NSDictionary *)args; + ++ (void)setCustomUserId:(NSString *)customUserId; ++ (void)unsetCustomUserId; ++ (void)setDeviceCustomUserId:(NSString *)customUserId; + ++ (void)revenue:(NSString *)currency amount:(double)amount; ++ (void)revenueWithArgs:(NSString *)currency amount:(double)amount args:(NSDictionary *)args; ++ (void)customRevenue:(NSString *)eventName currency:(NSString *)currency amount:(double)amount; ++ (void)customRevenueWithArgs:(NSString *)eventName currency:(NSString *)currency amount:(double)amount args:(NSDictionary *)args; + ++ (void)inAppPurchase:(NSString *)eventName purchase:(NSDictionary *)purchase; ++ (void)inAppPurchaseWithArgs:(NSString *)eventName purchase:(NSDictionary *)purchase args:(NSDictionary *)args; + ++ (void)setUninstallToken:(NSString *)token; + ++ (void)trackingOptIn; ++ (void)trackingUnder13; ++ (void)stopAllTracking; ++ (void)resumeAllTracking; ++ (BOOL)isAllTrackingStopped; + ++ (void)limitDataSharing:(BOOL)shouldLimit; ++ (BOOL)getLimitDataSharing; + ++ (BOOL)setGlobalProperty:(NSString *)key value:(NSString *)value overrideExisting:(BOOL)override; ++ (void)unsetGlobalProperty:(NSString *)key; ++ (void)clearGlobalProperties; ++ (NSDictionary *)getGlobalProperties; + ++ (BOOL)skanUpdateConversionValue:(NSInteger)conversionValue; ++ (void)skanUpdateConversionValues:(NSInteger)conversionValue coarse:(NSInteger)coarse lock:(BOOL)lock; ++ (NSNumber *)skanGetConversionValue; ++ (void)skanRegisterAppForAdNetworkAttribution; + ++ (void)handlePushNotification:(NSDictionary *)pushNotificationPayload; + ++ (void)setLimitAdvertisingIdentifiers:(BOOL)enabled; + ++ (void)createReferrerShortLink:(NSString *)baseLink + referrerName:(NSString *)referrerName + referrerId:(NSString *)referrerId + passthroughParams:(NSDictionary *)passthroughParams + completionHandler:(void(^)(NSString *result, NSString *error))completionHandler; + ++ (NSString *)dictionaryToJSONString:(NSDictionary *)dictionary; ++ (NSDictionary *)jsonStringToDictionary:(NSString *)jsonString; ++ (void)setReactSDKVersion:(NSString*)wrapper version:(NSString*)version; + ++ (NSArray *)supportedEvents; +@end + diff --git a/ios/SingularHelper.m b/ios/SingularHelper.m new file mode 100644 index 0000000..0c3dc5d --- /dev/null +++ b/ios/SingularHelper.m @@ -0,0 +1,253 @@ +#import "SingularHelper.h" +#import +#import + +@implementation SingularHelper + +#pragma mark - Initialization + ++ (void)initWithConfig:(SingularConfig *)config { + [Singular start:config]; +} + ++ (void)setSessionTimeout:(int)sessionTimeout { + [Singular setSessionTimeout:sessionTimeout]; +} + +#pragma mark - Event Tracking + ++ (void)event:(NSString *)eventName { + [Singular event:eventName]; +} + ++ (void)eventWithArgs:(NSString *)eventName args:(NSDictionary *)args { + [Singular event:eventName withArgs:args]; +} + +#pragma mark - User Management + ++ (void)setCustomUserId:(NSString *)customUserId { + [Singular setCustomUserId:customUserId]; +} + ++ (void)unsetCustomUserId { + [Singular unsetCustomUserId]; +} + ++ (void)setDeviceCustomUserId:(NSString *)customUserId { + [Singular setDeviceCustomUserId:customUserId]; +} + +#pragma mark - Revenue Tracking + ++ (void)revenue:(NSString *)currency amount:(double)amount { + [Singular revenue:currency amount:amount]; +} + ++ (void)revenueWithArgs:(NSString *)currency amount:(double)amount args:(NSDictionary *)args { + [Singular revenue:currency amount:amount withAttributes:args]; +} + ++ (void)customRevenue:(NSString *)eventName currency:(NSString *)currency amount:(double)amount { + [Singular customRevenue:eventName currency:currency amount:amount]; +} + ++ (void)customRevenueWithArgs:(NSString *)eventName currency:(NSString *)currency amount:(double)amount args:(NSDictionary *)args { + [Singular customRevenue:eventName currency:currency amount:amount withAttributes:args]; +} + +#pragma mark - In-App Purchases + ++ (void)inAppPurchase:(NSString *)eventName purchase:(NSDictionary *)purchase { + [Singular event:eventName withArgs:purchase]; +} + ++ (void)inAppPurchaseWithArgs:(NSString *)eventName purchase:(NSDictionary *)purchase args:(NSDictionary *)args { + NSMutableDictionary *merged = [args mutableCopy]; + [merged addEntriesFromDictionary:purchase]; + [Singular event:eventName withArgs:merged]; +} + +#pragma mark - Push Notifications + ++ (void)setUninstallToken:(NSString *)token { + NSData *tokenData = [self convertHexStringToDataBytes:token]; + [Singular registerDeviceTokenForUninstall:tokenData]; +} + ++ (NSData *)convertHexStringToDataBytes:(NSString *)hexString { + if([hexString length] % 2 != 0) { + return nil; + } + + const char *chars = [hexString UTF8String]; + int index = 0, length = (int)[hexString length]; + + NSMutableData *data = [NSMutableData dataWithCapacity:length / 2]; + char byteChars[3] = {'\0','\0','\0'}; + unsigned long wholeByte; + + while (index < length) { + byteChars[0] = chars[index++]; + byteChars[1] = chars[index++]; + wholeByte = strtoul(byteChars, NULL, 16); + [data appendBytes:&wholeByte length:1]; + } + + return data; +} + +#pragma mark - Tracking Control + ++ (void)trackingOptIn { + [Singular trackingOptIn]; +} + ++ (void)trackingUnder13 { + [Singular trackingUnder13]; +} + ++ (void)stopAllTracking { + [Singular stopAllTracking]; +} + ++ (void)resumeAllTracking { + [Singular resumeAllTracking]; +} + ++ (BOOL)isAllTrackingStopped { + return [Singular isAllTrackingStopped]; +} + +#pragma mark - Data Sharing + ++ (void)limitDataSharing:(BOOL)shouldLimit { + [Singular limitDataSharing:shouldLimit]; +} + ++ (BOOL)getLimitDataSharing { + return [Singular getLimitDataSharing]; +} + +#pragma mark - Global Properties + ++ (BOOL)setGlobalProperty:(NSString *)key value:(NSString *)value overrideExisting:(BOOL)override { + return [Singular setGlobalProperty:key andValue:value overrideExisting:override]; +} + ++ (void)unsetGlobalProperty:(NSString *)key { + [Singular unsetGlobalProperty:key]; +} + ++ (void)clearGlobalProperties { + [Singular clearGlobalProperties]; +} + ++ (NSDictionary *)getGlobalProperties { + NSDictionary *properties = [Singular getGlobalProperties]; + return properties ?: @{}; +} + +#pragma mark - SKAN Methods + ++ (BOOL)skanUpdateConversionValue:(NSInteger)conversionValue { + BOOL success = [Singular skanUpdateConversionValue:conversionValue]; + return success; +} + ++ (void)skanUpdateConversionValues:(NSInteger)conversionValue coarse:(NSInteger)coarse lock:(BOOL)lock { + [Singular skanUpdateConversionValue:conversionValue coarse:coarse lock:lock]; +} + ++ (NSNumber *)skanGetConversionValue { + NSNumber *value = [Singular skanGetConversionValue]; + return value; +} + ++ (void)skanRegisterAppForAdNetworkAttribution { + [Singular skanRegisterAppForAdNetworkAttribution]; +} + +#pragma mark - Push Notifications + ++ (void)handlePushNotification:(NSDictionary *)pushNotificationPayload { + [Singular handlePushNotification:pushNotificationPayload]; +} + +#pragma mark - Advertising Identifiers + ++ (void)setLimitAdvertisingIdentifiers:(BOOL)enabled { + [Singular setLimitAdvertisingIdentifiers:enabled]; +} + +#pragma mark - Short Links + ++ (void)createReferrerShortLink:(NSString *)baseLink + referrerName:(NSString *)referrerName + referrerId:(NSString *)referrerId + passthroughParams:(NSDictionary *)passthroughParams + completionHandler:(void(^)(NSString *result, NSString *error))completionHandler { + [Singular createReferrerShortLink:baseLink + referrerName:referrerName + referrerId:referrerId + passthroughParams:passthroughParams + completionHandler:^(NSString *result, NSError *error) { + if (error) { + completionHandler(nil, error.localizedDescription); + } else { + completionHandler(result, nil); + } + }]; +} + +#pragma mark - Helper Methods + ++ (NSString *)dictionaryToJSONString:(NSDictionary *)dictionary { + if (!dictionary) { + return @"{}"; + } + + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:&error]; + + if (error) { + NSLog(@"Error converting dictionary to JSON: %@", error.localizedDescription); + return @"{}"; + } + + return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; +} + ++ (NSDictionary *)jsonStringToDictionary:(NSString *)jsonString { + if (!jsonString || [jsonString isEqualToString:@""]) { + return @{}; + } + + NSError *error; + NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; + + if (error) { + NSLog(@"Error parsing JSON string: %@", error.localizedDescription); + return @{}; + } + + return dictionary; +} + ++ (void)setReactSDKVersion:(NSString*)wrapper version:(NSString*)version { + [Singular setWrapperName:wrapper andVersion:version]; +} + ++ (NSArray *)supportedEvents { + return @[SINGULAR_LINK_HANDLER_CONST, + CONVERSION_VALUE_UPDATED_HANDLER_CONST, + SHORT_LINK_HANDLER_CONST, + CONVERSION_VALUES_UPDATED_HANDLER_CONST, + DEVICE_ATTRIBUTION_CALLBACK_HANDLER_CONST, + SDID_RECEIVED_CALLBACK_CONST, + SDID_SET_CALLBACK_CONST]; +} + +@end + diff --git a/js/NativeSingular.ts b/js/NativeSingular.ts new file mode 100644 index 0000000..2940543 --- /dev/null +++ b/js/NativeSingular.ts @@ -0,0 +1,120 @@ +import { TurboModule, TurboModuleRegistry } from 'react-native'; + +export type SerializableValue = boolean | number | string | null | SerializableArray | SerializableObject; + +export type SerializableArray = Array; + +export type SerializableObject = { + [key: string]: SerializableValue; +}; + +export interface SingularLinkParams { + deeplink: string; + passthrough: string; + isDeferred: boolean; + urlParameters: { [key: string]: string }; +} + +export interface SingularPurchase { + revenue: number; + currency: string; + productId?: string; + transactionId?: string; + receipt?: string; + receipt_signature?: string; +} + +export interface SingularAdData { + ad_platform: string; + ad_currency: string; + ad_revenue: number; + ad_mediation_platform?: string; + ad_type?: string; + ad_group_type?: string; + ad_impression_id?: string; + ad_placement_name?: string; + ad_unit_id?: string; + ad_unit_name?: string; + ad_group_id?: string; + ad_group_name?: string; + ad_group_priority?: number; + ad_precision?: string; + ad_placement_id?: string; +} + +export interface SingularConfig { + apikey: string; + secret: string; + sessionTimeout?: number; + customUserId?: string; + shortLinkResolveTimeout?: number; + skAdNetworkEnabled?: boolean; + clipboardAttribution?: boolean; + manualSkanConversionManagement?: boolean; + waitForTrackingAuthorizationWithTimeoutInterval?: number; + customSdid?: string; + limitDataSharing?: boolean | null; + globalProperties?: SerializableObject; + collectOAID?: boolean; + enableLogging?: boolean; + espDomains?: string[]; + facebookAppId?: string; + pushNotificationsLinkPaths?: string[][]; + brandedDomains?: string[]; + limitAdvertisingIdentifiers?: boolean; + enableOdmWithTimeoutInterval?: number; +} + +export interface Spec extends TurboModule { + init(config: SingularConfig): void; + + // Required for emitting events - these are called by RN internally + addListener(eventType: string): void; + removeListeners(count: number): void; + + event(eventName: string): void; + eventWithArgs(eventName: string, args: SerializableObject): void; + + setCustomUserId(customUserId: string): void; + unsetCustomUserId(): void; + setDeviceCustomUserId(customUserId: string): void; + + revenue(currency: string, amount: number): void; + revenueWithArgs(currency: string, amount: number, args: SerializableObject): void; + customRevenue(eventName: string, currency: string, amount: number): void; + customRevenueWithArgs(eventName: string, currency: string, amount: number, args: SerializableObject): void; + + inAppPurchase(eventName: string, purchase: SingularPurchase): void; + inAppPurchaseWithArgs(eventName: string, purchase: SingularPurchase, args: SerializableObject): void; + + setUninstallToken(token: string): void; + + trackingOptIn(): void; + trackingUnder13(): void; + stopAllTracking(): void; + resumeAllTracking(): void; + isAllTrackingStopped(): boolean; + + limitDataSharing(shouldLimitDataSharing: boolean): void; + getLimitDataSharing(): boolean; + + setGlobalProperty(key: string, value: string, overrideExisting: boolean): boolean; + unsetGlobalProperty(key: string): void; + clearGlobalProperties(): void; + getGlobalProperties(): SerializableObject; + + createReferrerShortLink(baseLink: string, referrerName: string, referrerId: string, passthroughParams: SerializableObject, completionHandler: (result: string, error: string) => void): void; + + adRevenue(adData: SingularAdData): void; + + setLimitAdvertisingIdentifiers(enabled: boolean): void; + + // iOS-specific methods + skanUpdateConversionValue(conversionValue: number): boolean; + skanUpdateConversionValues(conversionValue: number, coarse: number, lock: boolean): void; + skanGetConversionValue(): number | null; + skanRegisterAppForAdNetworkAttribution(): void; + handlePushNotification(pushNotificationPayload: SerializableObject): void; +} + +export default TurboModuleRegistry.getEnforcing('SingularBridge'); diff --git a/package.json b/package.json index 4ff72d5..3f6dea8 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,31 @@ { "name": "singular-react-native", - "version": "3.9.0", + "version": "4.0.0", + "description": "Singular React Native SDK", + "main": "index.js", + "codegenConfig": { + "name": "NativeSingular", + "type": "modules", + "jsSrcsDir": "./js", + "android": { + "javaPackageName": "net.singular.react_native" + }, + "ios": { + "outputPath": "ios/build/generated/ios" + } + }, "peerDependencies": { + "react": ">=16.0.0", "react-native": ">=0.46.4" }, "files": [ "android/src", "android/build.gradle", "ios/SingularBridge.h", - "ios/SingularBridge.m", + "ios/SingularBridgeOldArch.m", + "ios/SingularBridgeNewArch.mm", + "ios/SingularHelper.h", + "ios/SingularHelper.m", "ios/SingularReactNative.xcodeproj/project.pbxproj", "Singular-React-Native.podspec", "Singular.js", @@ -19,15 +36,18 @@ "Events.js", "index.js", "app.plugin.js", - "index.d.ts" + "index.d.ts", + "js/NativeSingular.ts", + "build/generated/ios/", + "build/generated/android/" ], "types": "./index.d.ts", "devDependencies": { - "@babel/core": "7.7.4", - "@babel/runtime": "7.7.4", - "eslint": "6.7.2", - "jest": "24.9.0", - "react-native": "0.61.5", - "react": "16.9.0" + "@babel/core": "^7.7.4", + "@babel/runtime": "^7.7.4", + "eslint": "^6.7.2", + "jest": "^24.9.0", + "react-native": "^0.61.5", + "react": "^16.9.0" } }