From b1d204bf229ec8627873949720c31633cb2fccda Mon Sep 17 00:00:00 2001 From: Amit kremer Date: Sun, 29 Sep 2024 09:29:35 +0300 Subject: [PATCH 1/9] add minimum rn version --- README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index fafe8f29..5e946225 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ - Android AppsFlyer SDK **v6.15.1** - iOS AppsFlyer SDK **v6.15.1** +- Tested with React-Native **v0.62.0** (older versions might be supported) ## โ—โ— Breaking changes when updating to v6.x.xโ—โ— @@ -46,24 +47,28 @@ And removed the following ones: If you have used 1 of the removed APIs, please check the integration guide for the updated instructions. - --- +--- + +## ๐Ÿš€ Getting Started - ## ๐Ÿš€ Getting Started - [Installation](/Docs/RN_Installation.md) -- [***Expo*** Installation](/Docs/RN_ExpoInstallation.md) +- [**_Expo_** Installation](/Docs/RN_ExpoInstallation.md) - [Integration](/Docs/RN_Integration.md) - [Test integration](/Docs/RN_Testing.md) - [In-app events](/Docs/RN_InAppEvents.md) - [Uninstall measurement](/Docs/RN_UninstallMeasurement.md) - [Send consent for DMA compliance](/Docs/RN_CMP.md) -## ๐Ÿ”— Deep Linking + +## ๐Ÿ”— Deep Linking + - [Integration](/Docs/RN_DeepLinkIntegrate.md) -- [***Expo*** Integration](/Docs/RN_ExpoDeepLinkIntegration.md) +- [**_Expo_** Integration](/Docs/RN_ExpoDeepLinkIntegration.md) - [Unified Deep Link (UDL)](/Docs/RN_UnifiedDeepLink.md) - [User Invite](/Docs/RN_UserInvite.md) + ## ๐Ÿงช Sample Apps + - [React-Native Sample App](/demos/appsflyer-react-native-app) - [๐Ÿ†• Expo Sample App](https://github.com/AppsFlyerSDK/appsflyer-expo-sample-app) ### [API reference](/Docs/RN_API.md) - From 87774e4522130603eb6d9693db75c1e7fbfaf1b7 Mon Sep 17 00:00:00 2001 From: Amit Levy Date: Mon, 30 Sep 2024 15:53:18 +0300 Subject: [PATCH 2/9] PurchaseConnector Models and RN wrapper and native bridge --- PurchaseConnector/constants/constants.ts | 23 + .../models/auto_renewing_plan.ts | 34 + .../models/canceled_state_context.ts | 141 ++++ .../models/deferred_item_replacement.ts | 21 + .../models/external_account_identifiers.ts | 39 + .../in_app_purchase_validation_result.ts | 34 + PurchaseConnector/models/ios_errors.ts | 52 ++ PurchaseConnector/models/jvm_throwable.ts | 66 ++ .../models/missing_configuration_exception.ts | 5 + PurchaseConnector/models/money_model.ts | 29 + PurchaseConnector/models/offer_details.ts | 33 + .../models/paused_state_context.ts | 21 + PurchaseConnector/models/prepaid_plan.ts | 21 + PurchaseConnector/models/product_purchase.ts | 99 +++ .../models/subscribe_with_google_info.ts | 44 + .../subscription_item_price_change_details.ts | 51 ++ .../models/subscription_purchase.ts | 91 +++ .../models/subscription_purchase_line_item.ts | 66 ++ .../models/subscription_validation_result.ts | 50 ++ PurchaseConnector/models/test_purchase.ts | 13 + .../models/validation_failure_data.ts | 25 + .../utils/connector_callbacks.ts | 14 + android/.project | 6 - android/build.gradle | 9 +- .../reactnative/PCAppsFlyerModule.java | 443 ++++++++++ .../reactnative/PCAppsFlyerPackage.java | 32 + .../reactnative/RNAppsFlyerConstants.java | 4 + .../android/app/build.gradle | 2 + .../android/gradle.properties | 1 + .../components/AppsFlyer.js | 52 +- .../components/HomeScreen.js | 2 + demos/appsflyer-react-native-app/ios/Podfile | 3 + .../metro.config.js | 8 + demos/appsflyer-react-native-app/package.json | 2 +- index.d.ts | 523 +++++++----- index.js | 759 +++++++++++------- ios/PCAppsFlyer.h | 16 + ios/PCAppsFlyer.m | 99 +++ react-native-appsflyer.podspec | 10 +- react-native.config.js | 10 +- 40 files changed, 2429 insertions(+), 524 deletions(-) create mode 100644 PurchaseConnector/constants/constants.ts create mode 100644 PurchaseConnector/models/auto_renewing_plan.ts create mode 100644 PurchaseConnector/models/canceled_state_context.ts create mode 100644 PurchaseConnector/models/deferred_item_replacement.ts create mode 100644 PurchaseConnector/models/external_account_identifiers.ts create mode 100644 PurchaseConnector/models/in_app_purchase_validation_result.ts create mode 100644 PurchaseConnector/models/ios_errors.ts create mode 100644 PurchaseConnector/models/jvm_throwable.ts create mode 100644 PurchaseConnector/models/missing_configuration_exception.ts create mode 100644 PurchaseConnector/models/money_model.ts create mode 100644 PurchaseConnector/models/offer_details.ts create mode 100644 PurchaseConnector/models/paused_state_context.ts create mode 100644 PurchaseConnector/models/prepaid_plan.ts create mode 100644 PurchaseConnector/models/product_purchase.ts create mode 100644 PurchaseConnector/models/subscribe_with_google_info.ts create mode 100644 PurchaseConnector/models/subscription_item_price_change_details.ts create mode 100644 PurchaseConnector/models/subscription_purchase.ts create mode 100644 PurchaseConnector/models/subscription_purchase_line_item.ts create mode 100644 PurchaseConnector/models/subscription_validation_result.ts create mode 100644 PurchaseConnector/models/test_purchase.ts create mode 100644 PurchaseConnector/models/validation_failure_data.ts create mode 100644 PurchaseConnector/utils/connector_callbacks.ts create mode 100644 android/src/main/java/com/appsflyer/reactnative/PCAppsFlyerModule.java create mode 100644 android/src/main/java/com/appsflyer/reactnative/PCAppsFlyerPackage.java create mode 100644 ios/PCAppsFlyer.h create mode 100644 ios/PCAppsFlyer.m diff --git a/PurchaseConnector/constants/constants.ts b/PurchaseConnector/constants/constants.ts new file mode 100644 index 00000000..78aacf6a --- /dev/null +++ b/PurchaseConnector/constants/constants.ts @@ -0,0 +1,23 @@ +class AppsFlyerConstants { + static readonly RE_CONFIGURE_ERROR_MSG: string = "[PurchaseConnector] Re configure instance is not permitted. Returned the existing instance"; + static readonly MISSING_CONFIGURATION_EXCEPTION_MSG: string = "Could not create an instance without configuration"; + + // Adding method constants + static readonly SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER: string = + "onSubscriptionValidationResult"; + static readonly IN_APP_VALIDATION_RESULT_LISTENER: string = + "onInAppPurchaseValidationResult"; + static readonly DID_RECEIVE_PURCHASE_REVENUE_VALIDATION_INFO: string = + "onDidReceivePurchaseRevenueValidationInfo"; + + // Adding key constants + static readonly RESULT: string = "result"; + static readonly ERROR: string = "error"; + static readonly VALIDATION_INFO: string = "validationInfo"; + static readonly CONFIGURE_KEY: string = "configure"; + static readonly LOG_SUBS_KEY: string = "logSubscriptions"; + static readonly LOG_IN_APP_KEY: string = "logInApps"; + static readonly SANDBOX_KEY: string = "sandbox"; + } + + export default AppsFlyerConstants; \ No newline at end of file diff --git a/PurchaseConnector/models/auto_renewing_plan.ts b/PurchaseConnector/models/auto_renewing_plan.ts new file mode 100644 index 00000000..fe84bdfd --- /dev/null +++ b/PurchaseConnector/models/auto_renewing_plan.ts @@ -0,0 +1,34 @@ +import { SubscriptionItemPriceChangeDetailsJson, SubscriptionItemPriceChangeDetails } from "./subscription_item_price_change_details"; + +export type AutoRenewingPlanJson = { + autoRenewEnabled?: boolean; + priceChangeDetails?: SubscriptionItemPriceChangeDetailsJson; +}; + +export class AutoRenewingPlan { + autoRenewEnabled?: boolean; + priceChangeDetails?: SubscriptionItemPriceChangeDetails; + + constructor( + autoRenewEnabled?: boolean, + priceChangeDetails?: SubscriptionItemPriceChangeDetails + ) { + this.autoRenewEnabled = autoRenewEnabled; + this.priceChangeDetails = priceChangeDetails; + } + + static fromJson(json: AutoRenewingPlanJson): AutoRenewingPlan { + return new AutoRenewingPlan( + json.autoRenewEnabled, + json.priceChangeDetails && + SubscriptionItemPriceChangeDetails.fromJson(json.priceChangeDetails) + ); + } + + toJson(): AutoRenewingPlanJson { + return { + autoRenewEnabled: this.autoRenewEnabled, + priceChangeDetails: this.priceChangeDetails?.toJson(), + }; + } +} diff --git a/PurchaseConnector/models/canceled_state_context.ts b/PurchaseConnector/models/canceled_state_context.ts new file mode 100644 index 00000000..17f5f075 --- /dev/null +++ b/PurchaseConnector/models/canceled_state_context.ts @@ -0,0 +1,141 @@ +export class CanceledStateContext { + developerInitiatedCancellation?: DeveloperInitiatedCancellation; + replacementCancellation?: ReplacementCancellation; + systemInitiatedCancellation?: SystemInitiatedCancellation; + userInitiatedCancellation?: UserInitiatedCancellation; + + constructor( + developerInitiatedCancellation?: DeveloperInitiatedCancellation, + replacementCancellation?: ReplacementCancellation, + systemInitiatedCancellation?: SystemInitiatedCancellation, + userInitiatedCancellation?: UserInitiatedCancellation + ) { + this.developerInitiatedCancellation = developerInitiatedCancellation; + this.replacementCancellation = replacementCancellation; + this.systemInitiatedCancellation = systemInitiatedCancellation; + this.userInitiatedCancellation = userInitiatedCancellation; + } + + static fromJson(json: any): CanceledStateContext { + return new CanceledStateContext( + json.developerInitiatedCancellation != null + ? DeveloperInitiatedCancellation.fromJson( + json.developerInitiatedCancellation + ) + : undefined, + json.replacementCancellation != null + ? ReplacementCancellation.fromJson(json.replacementCancellation) + : undefined, + json.systemInitiatedCancellation != null + ? SystemInitiatedCancellation.fromJson(json.systemInitiatedCancellation) + : undefined, + json.userInitiatedCancellation != null + ? UserInitiatedCancellation.fromJson(json.userInitiatedCancellation) + : undefined + ); + } + + toJson(): Record { + return { + developerInitiatedCancellation: + this.developerInitiatedCancellation?.toJson(), + replacementCancellation: this.replacementCancellation?.toJson(), + systemInitiatedCancellation: this.systemInitiatedCancellation?.toJson(), + userInitiatedCancellation: this.userInitiatedCancellation?.toJson(), + }; + } +} + +/** + * TODO: Need to check each state context further... + */ +class DeveloperInitiatedCancellation { + constructor() {} + + static fromJson(json: any): DeveloperInitiatedCancellation { + // Here you would implement the conversion from JSON to DeveloperInitiatedCancellation instance + return new DeveloperInitiatedCancellation(); + } + + toJson(): Record { + // Here you would implement the conversion from DeveloperInitiatedCancellation instance to JSON + return {}; + } +} + +class ReplacementCancellation { + constructor() {} + + static fromJson(json: any): ReplacementCancellation { + // Here you would implement the conversion from JSON to ReplacementCancellation instance + return new ReplacementCancellation(); + } + + toJson(): Record { + return {}; + } +} + +class SystemInitiatedCancellation { + constructor() {} + + static fromJson(json: any): SystemInitiatedCancellation { + // Here you would implement the conversion from JSON to SystemInitiatedCancellation instance + return new SystemInitiatedCancellation(); + } + + toJson(): Record { + // Here you would implement the conversion from SystemInitiatedCancellation instance to JSON + return {}; + } +} + +class UserInitiatedCancellation { + cancelSurveyResult?: CancelSurveyResult; // Made optional as per Dart's CancelSurveyResult? declaration + cancelTime: string; + + constructor( + cancelSurveyResult: CancelSurveyResult | undefined, + cancelTime: string + ) { + this.cancelSurveyResult = cancelSurveyResult; + this.cancelTime = cancelTime; + } + + static fromJson(json: any): UserInitiatedCancellation { + return new UserInitiatedCancellation( + json.cancelSurveyResult != null + ? CancelSurveyResult.fromJson(json.cancelSurveyResult) + : undefined, + json.cancelTime + ); + } + + toJson(): Record { + return { + cancelSurveyResult: this.cancelSurveyResult?.toJson(), + cancelTime: this.cancelTime, + }; + } +} + +class CancelSurveyResult { + reason: string; + reasonUserInput: string; + + constructor(reason: string, reasonUserInput: string) { + this.reason = reason; + this.reasonUserInput = reasonUserInput; + } + + static fromJson(json: any): CancelSurveyResult { + return new CancelSurveyResult(json.reason, json.reasonUserInput); + } + + toJson(): Record { + return { + reason: this.reason, + reasonUserInput: this.reasonUserInput, + }; + } +} diff --git a/PurchaseConnector/models/deferred_item_replacement.ts b/PurchaseConnector/models/deferred_item_replacement.ts new file mode 100644 index 00000000..5550a18b --- /dev/null +++ b/PurchaseConnector/models/deferred_item_replacement.ts @@ -0,0 +1,21 @@ +export type DeferredItemReplacementJson = { + productId: string; +}; + +export class DeferredItemReplacement { + productId: string; + + constructor(productId: string) { + this.productId = productId; + } + + static fromJson(json: DeferredItemReplacementJson): DeferredItemReplacement { + return new DeferredItemReplacement(json.productId); + } + + toJson(): DeferredItemReplacementJson { + return { + productId: this.productId, + }; + } +} diff --git a/PurchaseConnector/models/external_account_identifiers.ts b/PurchaseConnector/models/external_account_identifiers.ts new file mode 100644 index 00000000..9ee3f0bf --- /dev/null +++ b/PurchaseConnector/models/external_account_identifiers.ts @@ -0,0 +1,39 @@ +export type ExternalAccountIdentifiersJson = { + externalAccountId: string; + obfuscatedExternalAccountId: string; + obfuscatedExternalProfileId: string; +}; + +export class ExternalAccountIdentifiers { + externalAccountId: string; + obfuscatedExternalAccountId: string; + obfuscatedExternalProfileId: string; + + constructor( + externalAccountId: string, + obfuscatedExternalAccountId: string, + obfuscatedExternalProfileId: string + ) { + this.externalAccountId = externalAccountId; + this.obfuscatedExternalAccountId = obfuscatedExternalAccountId; + this.obfuscatedExternalProfileId = obfuscatedExternalProfileId; + } + + static fromJson( + json: ExternalAccountIdentifiersJson + ): ExternalAccountIdentifiers { + return new ExternalAccountIdentifiers( + json.externalAccountId, + json.obfuscatedExternalAccountId, + json.obfuscatedExternalProfileId + ); + } + + toJson(): ExternalAccountIdentifiersJson { + return { + externalAccountId: this.externalAccountId, + obfuscatedExternalAccountId: this.obfuscatedExternalAccountId, + obfuscatedExternalProfileId: this.obfuscatedExternalProfileId, + }; + } +} diff --git a/PurchaseConnector/models/in_app_purchase_validation_result.ts b/PurchaseConnector/models/in_app_purchase_validation_result.ts new file mode 100644 index 00000000..fd4567d6 --- /dev/null +++ b/PurchaseConnector/models/in_app_purchase_validation_result.ts @@ -0,0 +1,34 @@ +import { ProductPurchase } from "./product_purchase"; +import { ValidationFailureData } from "./validation_failure_data"; + +export class InAppPurchaseValidationResult { + success: boolean; + productPurchase?: ProductPurchase; + failureData?: ValidationFailureData; + + constructor( + success: boolean, + productPurchase?: ProductPurchase, + failureData?: ValidationFailureData + ) { + this.success = success; + this.productPurchase = productPurchase; + this.failureData = failureData; + } + + static fromJson(json: any): InAppPurchaseValidationResult { + return new InAppPurchaseValidationResult( + json.success, + json.productPurchase, + json.failureData + ); + } + + toJson(): any { + return { + success: this.success, + productPurchase: this.productPurchase, + failureData: this.failureData, + }; + } + } \ No newline at end of file diff --git a/PurchaseConnector/models/ios_errors.ts b/PurchaseConnector/models/ios_errors.ts new file mode 100644 index 00000000..aa6231d1 --- /dev/null +++ b/PurchaseConnector/models/ios_errors.ts @@ -0,0 +1,52 @@ +// TypeScript class for IOS Error +export class IosError { + localizedDescription: string; + domain: string; + code: number; + + constructor(localizedDescription: string, domain: string, code: number) { + this.localizedDescription = localizedDescription; + this.domain = domain; + this.code = code; + } + + // Converts the class instance to a JSON object + toJson(): object { + return { + localizedDescription: this.localizedDescription, + domain: this.domain, + code: this.code, + }; + } + + // Creates an instance of the class from a JSON object + static fromJson(json: any): IosError { + return new IosError(json.localizedDescription, json.domain, json.code); + } +} + +/** + * Usage example: + * // Creating an instance of IosError + * const iosError = new IosError('An error occurred.', 'com.example.domain', 100); + * + * // Display information about the IOS error + * console.log(iosError.localizedDescription); // Outputs: An error occurred. + * console.log(iosError.domain); // Outputs: com.example.domain + * console.log(iosError.code); // Outputs: 100 + * + * // Serializing IosError instance to a JSON object + * const iosErrorJson = iosError.toJson(); + * console.log(iosErrorJson); // Outputs: { localizedDescription: 'An error occurred.', domain: 'com.example.domain', code: 100 } + * + * // Sample JSON objects + * const iosErrorData = { + * localizedDescription: 'A network error occurred.', + * domain: 'com.example.network', + * code: 404 + * }; + * + * // Deserializing the parsed JSON into instance of IosError + * const deserializedIosError = IosError.fromJson(iosErrorData); + * console.log(deserializedIosError); + */ diff --git a/PurchaseConnector/models/jvm_throwable.ts b/PurchaseConnector/models/jvm_throwable.ts new file mode 100644 index 00000000..3ac640cc --- /dev/null +++ b/PurchaseConnector/models/jvm_throwable.ts @@ -0,0 +1,66 @@ +// TypeScript class for JVM Throwable +export class JVMThrowable { + type: string; + message: string; + stacktrace: string; + cause: JVMThrowable | null; + + constructor( + type: string, + message: string, + stacktrace: string, + cause: JVMThrowable | null + ) { + this.type = type; + this.message = message; + this.stacktrace = stacktrace; + this.cause = cause; + } + + // Converts the class instance to a JSON object + toJson(): object { + return { + type: this.type, + message: this.message, + stacktrace: this.stacktrace, + cause: this.cause?.toJson(), + }; + } + + // Creates an instance of the class from a JSON object + static fromJson(json: any): JVMThrowable { + return new JVMThrowable( + json.type, + json.message, + json.stacktrace, + json.cause ? JVMThrowable.fromJson(json.cause) : null + ); + } +} + +/** + * Usage example: + * // Creating an instance of JVMThrowable + * const jvmThrowable = new JVMThrowable('ExceptionType', 'An exception occurred', 'stacktraceString', null); + * + * // Display information about the JVM throwable + * console.log(jvmThrowable.type); // Outputs: ExceptionType + * console.log(jvmThrowable.message); // Outputs: An exception occurred + * console.log(jvmThrowable.stacktrace); // Outputs: stacktraceString + * console.log(jvmThrowable.cause); // Outputs: null (since no cause is provided here) + * + * // Serializing JVMThrowable instance to a JSON object + * const jvmThrowableJson = jvmThrowable.toJson(); + * console.log(jvmThrowableJson); // Outputs: { type: 'ExceptionType', message: 'An exception occurred', stacktrace: 'stacktraceString', cause: null } + * + * const jvmThrowableData = { + * type: 'RuntimeException', + * message: 'Failed to load resource', + * stacktrace: 'stacktrace info here', + * cause: null // Alternatively, you could nest another throwable here if there is one + * }; + * + * // Deserializing the parsed JSON into instance JVMThrowable + * const deserializedJVMThrowable = JVMThrowable.fromJson(jvmThrowableData); + * console.log(deserializedJVMThrowable); + */ diff --git a/PurchaseConnector/models/missing_configuration_exception.ts b/PurchaseConnector/models/missing_configuration_exception.ts new file mode 100644 index 00000000..2cd4c8cd --- /dev/null +++ b/PurchaseConnector/models/missing_configuration_exception.ts @@ -0,0 +1,5 @@ +export class MissingConfigurationException extends Error { + constructor() { + super("Missing configuration for PurchaseConnector"); + } +} diff --git a/PurchaseConnector/models/money_model.ts b/PurchaseConnector/models/money_model.ts new file mode 100644 index 00000000..d61ab985 --- /dev/null +++ b/PurchaseConnector/models/money_model.ts @@ -0,0 +1,29 @@ +export type MoneyArgs = { + currencyCode: string; + nanos: number; + units: number; +}; + +export class Money { + currencyCode: string; + nanos: number; + units: number; + + constructor(currencyCode: string, nanos: number, units: number) { + this.currencyCode = currencyCode; + this.nanos = nanos; + this.units = units; + } + + static fromJson(json: MoneyArgs): Money { + return new Money(json.currencyCode, json.nanos, json.units); + } + + toJson(): MoneyArgs { + return { + currencyCode: this.currencyCode, + nanos: this.nanos, + units: this.units, + }; + } +} diff --git a/PurchaseConnector/models/offer_details.ts b/PurchaseConnector/models/offer_details.ts new file mode 100644 index 00000000..e9de947a --- /dev/null +++ b/PurchaseConnector/models/offer_details.ts @@ -0,0 +1,33 @@ +export type OfferDetailsJson = { + offerTags?: string[]; + basePlanId: string; + offerId?: string; +}; + +export class OfferDetails { + offerTags?: string[]; + basePlanId: string; + offerId?: string; + + constructor( + offerTags: string[] | undefined, + basePlanId: string, + offerId: string | undefined + ) { + this.offerTags = offerTags; + this.basePlanId = basePlanId; + this.offerId = offerId; + } + + static fromJson(json: OfferDetailsJson): OfferDetails { + return new OfferDetails(json.offerTags, json.basePlanId, json.offerId); + } + + toJson(): OfferDetailsJson { + return { + offerTags: this.offerTags, + basePlanId: this.basePlanId, + offerId: this.offerId, + }; + } +} diff --git a/PurchaseConnector/models/paused_state_context.ts b/PurchaseConnector/models/paused_state_context.ts new file mode 100644 index 00000000..522edfc8 --- /dev/null +++ b/PurchaseConnector/models/paused_state_context.ts @@ -0,0 +1,21 @@ +type PausedStateContextJson = { + autoResumeTime: string; +}; + +export class PausedStateContext { + autoResumeTime: string; + + constructor(autoResumeTime: string) { + this.autoResumeTime = autoResumeTime; + } + + static fromJson(json: PausedStateContextJson): PausedStateContext { + return new PausedStateContext(json.autoResumeTime); + } + + toJson(): PausedStateContextJson { + return { + autoResumeTime: this.autoResumeTime, + }; + } +} diff --git a/PurchaseConnector/models/prepaid_plan.ts b/PurchaseConnector/models/prepaid_plan.ts new file mode 100644 index 00000000..40d967e4 --- /dev/null +++ b/PurchaseConnector/models/prepaid_plan.ts @@ -0,0 +1,21 @@ +export type PrepaidPlanJson = { + allowExtendAfterTime?: string; +}; + +export class PrepaidPlan { + allowExtendAfterTime?: string; + + constructor(allowExtendAfterTime?: string) { + this.allowExtendAfterTime = allowExtendAfterTime; + } + + static fromJson(json: PrepaidPlanJson): PrepaidPlan { + return new PrepaidPlan(json.allowExtendAfterTime); + } + + toJson(): PrepaidPlanJson { + return { + allowExtendAfterTime: this.allowExtendAfterTime, + }; + } +} diff --git a/PurchaseConnector/models/product_purchase.ts b/PurchaseConnector/models/product_purchase.ts new file mode 100644 index 00000000..fd21ba0c --- /dev/null +++ b/PurchaseConnector/models/product_purchase.ts @@ -0,0 +1,99 @@ +export type ProductPurchaseArgs = { + kind: string; + purchaseTimeMillis: string; + purchaseState: number; + consumptionState: number; + developerPayload: string; + orderId: string; + purchaseType: number; + acknowledgementState: number; + purchaseToken: string; + productId: string; + quantity: number; + obfuscatedExternalAccountId: string; + obfuscatedExternalProfileId: string; + regionCode: string; +}; + +export class ProductPurchase { + kind: string; + purchaseTimeMillis: string; + purchaseState: number; + consumptionState: number; + developerPayload: string; + orderId: string; + purchaseType: number; + acknowledgementState: number; + purchaseToken: string; + productId: string; + quantity: number; + obfuscatedExternalAccountId: string; + obfuscatedExternalProfileId: string; + regionCode: string; + + constructor(args: ProductPurchaseArgs) { + this.kind = args.kind; + this.purchaseTimeMillis = args.purchaseTimeMillis; + this.purchaseState = args.purchaseState; + this.consumptionState = args.consumptionState; + this.developerPayload = args.developerPayload; + this.orderId = args.orderId; + this.purchaseType = args.purchaseType; + this.acknowledgementState = args.acknowledgementState; + this.purchaseToken = args.purchaseToken; + this.productId = args.productId; + this.quantity = args.quantity; + this.obfuscatedExternalAccountId = args.obfuscatedExternalAccountId; + this.obfuscatedExternalProfileId = args.obfuscatedExternalProfileId; + this.regionCode = args.regionCode; + } + + toJson(): Record { + return { + kind: this.kind, + purchaseTimeMillis: this.purchaseTimeMillis, + purchaseState: this.purchaseState, + consumptionState: this.consumptionState, + developerPayload: this.developerPayload, + orderId: this.orderId, + purchaseType: this.purchaseType, + acknowledgementState: this.acknowledgementState, + purchaseToken: this.purchaseToken, + productId: this.productId, + quantity: this.quantity, + obfuscatedExternalAccountId: this.obfuscatedExternalAccountId, + obfuscatedExternalProfileId: this.obfuscatedExternalProfileId, + regionCode: this.regionCode, + }; + } + + static fromJson(json: any): ProductPurchase { + return new ProductPurchase({ + kind: json.kind as string, + purchaseTimeMillis: json.purchaseTimeMillis as string, + purchaseState: json.purchaseState as number, + consumptionState: json.consumptionState as number, + developerPayload: json.developerPayload as string, + orderId: json.orderId as string, + purchaseType: json.purchaseType as number, + acknowledgementState: json.acknowledgementState as number, + purchaseToken: json.purchaseToken as string, + productId: json.productId as string, + quantity: json.quantity as number, + obfuscatedExternalAccountId: json.obfuscatedExternalAccountId as string, + obfuscatedExternalProfileId: json.obfuscatedExternalProfileId as string, + regionCode: json.regionCode as string, + }); + } +} + +/** + * // Usage: + * // To convert to JSON: + * const productPurchaseInstance = new ProductPurchase({ ...args }); + * const json = productPurchaseInstance.toJson(); + * + * // To convert from JSON: + * const json = {...json }; + * const productPurchaseInstance = ProductPurchase.fromJson(json); + */ diff --git a/PurchaseConnector/models/subscribe_with_google_info.ts b/PurchaseConnector/models/subscribe_with_google_info.ts new file mode 100644 index 00000000..43737d6d --- /dev/null +++ b/PurchaseConnector/models/subscribe_with_google_info.ts @@ -0,0 +1,44 @@ +export type SubscribeWithGoogleInfoJson = { + emailAddress: string; + familyName: string; + givenName: string; + profileId: string; + profileName: string; +}; + +export class SubscribeWithGoogleInfo { + emailAddress: string; + familyName: string; + givenName: string; + profileId: string; + profileName: string; + + constructor( + emailAddress: string, + familyName: string, + givenName: string, + profileId: string, + profileName: string + ) { + this.emailAddress = emailAddress; + this.familyName = familyName; + this.givenName = givenName; + this.profileId = profileId; + this.profileName = profileName; + } + + static fromJson(json: SubscribeWithGoogleInfoJson): SubscribeWithGoogleInfo { + return new SubscribeWithGoogleInfo( + json.emailAddress, + json.familyName, + json.givenName, + json.profileId, + json.profileName + ); + } + + //This is primitve types so we can use ...this (spread operator) + toJson(): SubscribeWithGoogleInfoJson { + return { ...this }; + } +} diff --git a/PurchaseConnector/models/subscription_item_price_change_details.ts b/PurchaseConnector/models/subscription_item_price_change_details.ts new file mode 100644 index 00000000..53ce6430 --- /dev/null +++ b/PurchaseConnector/models/subscription_item_price_change_details.ts @@ -0,0 +1,51 @@ +import { Money, MoneyArgs } from "./money_model"; + +export type SubscriptionItemPriceChangeDetailsJson = { + expectedNewPriceChargeTime: string; + priceChangeMode: string; + priceChangeState: string; + newPrice?: MoneyArgs; +}; + +export class SubscriptionItemPriceChangeDetails { + expectedNewPriceChargeTime: string; + priceChangeMode: string; + priceChangeState: string; + newPrice?: Money; + + constructor( + expectedNewPriceChargeTime: string, + priceChangeMode: string, + priceChangeState: string, + newPrice?: Money + ) { + this.expectedNewPriceChargeTime = expectedNewPriceChargeTime; + this.priceChangeMode = priceChangeMode; + this.priceChangeState = priceChangeState; + this.newPrice = newPrice; + } + + static fromJson( + json: SubscriptionItemPriceChangeDetailsJson + ): SubscriptionItemPriceChangeDetails { + const newPriceInstance = json.newPrice + ? Money.fromJson(json.newPrice) + : undefined; + + return new SubscriptionItemPriceChangeDetails( + json.expectedNewPriceChargeTime, + json.priceChangeMode, + json.priceChangeState, + newPriceInstance + ); + } + + toJson(): SubscriptionItemPriceChangeDetailsJson { + return { + expectedNewPriceChargeTime: this.expectedNewPriceChargeTime, + priceChangeMode: this.priceChangeMode, + priceChangeState: this.priceChangeState, + newPrice: this.newPrice ? this.newPrice.toJson() : undefined, + }; + } +} diff --git a/PurchaseConnector/models/subscription_purchase.ts b/PurchaseConnector/models/subscription_purchase.ts new file mode 100644 index 00000000..4c67ab15 --- /dev/null +++ b/PurchaseConnector/models/subscription_purchase.ts @@ -0,0 +1,91 @@ +import { CanceledStateContext } from "./canceled_state_context"; +import { ExternalAccountIdentifiers } from "./external_account_identifiers"; +import { PausedStateContext } from "./paused_state_context"; +import { SubscribeWithGoogleInfo } from "./subscribe_with_google_info"; +import { SubscriptionPurchaseLineItem } from "./subscription_purchase_line_item"; +import { TestPurchase } from "./test_purchase"; + +type SubscriptionPurchaseArgs = { + acknowledgementState: string; + canceledStateContext?: CanceledStateContext; + externalAccountIdentifiers?: ExternalAccountIdentifiers; + kind: string; + latestOrderId: string; + lineItems: SubscriptionPurchaseLineItem[]; + linkedPurchaseToken?: string; + pausedStateContext?: PausedStateContext; + regionCode: string; + startTime: string; + subscribeWithGoogleInfo?: SubscribeWithGoogleInfo; + subscriptionState: string; + testPurchase?: TestPurchase; +}; + +export class SubscriptionPurchase { + acknowledgementState: string; + canceledStateContext?: CanceledStateContext; + externalAccountIdentifiers?: ExternalAccountIdentifiers; + kind: string; + latestOrderId: string; + lineItems: SubscriptionPurchaseLineItem[]; + linkedPurchaseToken?: string; + pausedStateContext?: PausedStateContext; + regionCode: string; + startTime: string; + subscribeWithGoogleInfo?: SubscribeWithGoogleInfo; + subscriptionState: string; + testPurchase?: TestPurchase; + + constructor(args: SubscriptionPurchaseArgs) { + this.acknowledgementState = args.acknowledgementState; + this.canceledStateContext = args.canceledStateContext; + this.externalAccountIdentifiers = args.externalAccountIdentifiers; + this.kind = args.kind; + this.latestOrderId = args.latestOrderId; + this.lineItems = args.lineItems; + this.linkedPurchaseToken = args.linkedPurchaseToken; + this.pausedStateContext = args.pausedStateContext; + this.regionCode = args.regionCode; + this.startTime = args.startTime; + this.subscribeWithGoogleInfo = args.subscribeWithGoogleInfo; + this.subscriptionState = args.subscriptionState; + this.testPurchase = args.testPurchase; + } + + static fromJson(json: any): SubscriptionPurchase { + return new SubscriptionPurchase({ + acknowledgementState: json.acknowledgementState as string, + canceledStateContext: json.canceledStateContext, + externalAccountIdentifiers: json.externalAccountIdentifiers, + kind: json.kind as string, + latestOrderId: json.latestOrderId as string, + lineItems: json.lineItems, + linkedPurchaseToken: json.linkedPurchaseToken as string, + pausedStateContext: json.pausedStateContext, + regionCode: json.regionCode as string, + startTime: json.startTime as string, + subscribeWithGoogleInfo: json.subscribeWithGoogleInfo, + subscriptionState: json.subscriptionState as string, + testPurchase: json.testPurchase, + }); + } + + toJson(): Record { + return { + acknowledgementState: this.acknowledgementState, + canceledStateContext: this.canceledStateContext, + externalAccountIdentifiers: this.externalAccountIdentifiers, + kind: this.kind, + latestOrderId: this.latestOrderId, + lineItems: this.lineItems, + linkedPurchaseToken: this.linkedPurchaseToken, + pausedStateContext: this.pausedStateContext, + regionCode: this.regionCode, + startTime: this.startTime, + subscribeWithGoogleInfo: this.subscribeWithGoogleInfo, + subscriptionState: this.subscriptionState, + testPurchase: this.testPurchase, + }; + } +} + diff --git a/PurchaseConnector/models/subscription_purchase_line_item.ts b/PurchaseConnector/models/subscription_purchase_line_item.ts new file mode 100644 index 00000000..6d06d98c --- /dev/null +++ b/PurchaseConnector/models/subscription_purchase_line_item.ts @@ -0,0 +1,66 @@ +import { AutoRenewingPlanJson, AutoRenewingPlan } from "./auto_renewing_plan"; +import { DeferredItemReplacementJson, DeferredItemReplacement } from "./deferred_item_replacement"; +import { OfferDetailsJson, OfferDetails } from "./offer_details"; +import { PrepaidPlanJson, PrepaidPlan } from "./prepaid_plan"; + +export type SubscriptionPurchaseLineItemJson = { + productId: string; + expiryTime: string; + autoRenewingPlan?: AutoRenewingPlanJson; + deferredItemReplacement?: DeferredItemReplacementJson; + offerDetails?: OfferDetailsJson; + prepaidPlan?: PrepaidPlanJson; +}; + +export class SubscriptionPurchaseLineItem { + productId: string; + expiryTime: string; + autoRenewingPlan?: AutoRenewingPlan; + deferredItemReplacement?: DeferredItemReplacement; + offerDetails?: OfferDetails; + prepaidPlan?: PrepaidPlan; + + constructor( + productId: string, + expiryTime: string, + autoRenewingPlan?: AutoRenewingPlan, + deferredItemReplacement?: DeferredItemReplacement, + offerDetails?: OfferDetails, + prepaidPlan?: PrepaidPlan + ) { + this.productId = productId; + this.expiryTime = expiryTime; + this.autoRenewingPlan = autoRenewingPlan; + this.deferredItemReplacement = deferredItemReplacement; + this.offerDetails = offerDetails; + this.prepaidPlan = prepaidPlan; + } + + static fromJson( + json: SubscriptionPurchaseLineItemJson + ): SubscriptionPurchaseLineItem { + return new SubscriptionPurchaseLineItem( + json.productId, + json.expiryTime, + json.autoRenewingPlan + ? AutoRenewingPlan.fromJson(json.autoRenewingPlan) + : undefined, + json.deferredItemReplacement + ? DeferredItemReplacement.fromJson(json.deferredItemReplacement) + : undefined, + json.offerDetails ? OfferDetails.fromJson(json.offerDetails) : undefined, + json.prepaidPlan ? PrepaidPlan.fromJson(json.prepaidPlan) : undefined + ); + } + + toJson(): SubscriptionPurchaseLineItemJson { + return { + productId: this.productId, + expiryTime: this.expiryTime, + autoRenewingPlan: this.autoRenewingPlan?.toJson(), + deferredItemReplacement: this.deferredItemReplacement?.toJson(), + offerDetails: this.offerDetails?.toJson(), + prepaidPlan: this.prepaidPlan?.toJson(), + }; + } +} diff --git a/PurchaseConnector/models/subscription_validation_result.ts b/PurchaseConnector/models/subscription_validation_result.ts new file mode 100644 index 00000000..f9eb6ed4 --- /dev/null +++ b/PurchaseConnector/models/subscription_validation_result.ts @@ -0,0 +1,50 @@ +import { SubscriptionPurchase } from "./subscription_purchase"; +import { ValidationFailureData } from "./validation_failure_data"; + +type SubscriptionValidationResultArgs = { + success: boolean; + subscriptionPurchase?: SubscriptionPurchase; + failureData?: ValidationFailureData; +}; + +class SubscriptionValidationResult { + success: boolean; + subscriptionPurchase?: SubscriptionPurchase; + failureData?: ValidationFailureData; + + constructor( + success: boolean, + subscriptionPurchase?: SubscriptionPurchase, + failureData?: ValidationFailureData + ) { + this.success = success; + this.subscriptionPurchase = subscriptionPurchase; + this.failureData = failureData; + } + + static fromJson(json: { [key: string]: any }): SubscriptionValidationResult { + const subscriptionPurchaseInstance = json.subscriptionPurchase + ? SubscriptionPurchase.fromJson(json.subscriptionPurchase) + : undefined; + + const failureDataInstance = json.failureData + ? ValidationFailureData.fromJson(json.failureData) + : undefined; + + return new SubscriptionValidationResult( + json.success, + subscriptionPurchaseInstance, + failureDataInstance + ); + } + + toJson(): SubscriptionValidationResultArgs { + return { + success: this.success, + subscriptionPurchase: this.subscriptionPurchase, + failureData: this.failureData, + }; + } +} + +export default SubscriptionValidationResult; diff --git a/PurchaseConnector/models/test_purchase.ts b/PurchaseConnector/models/test_purchase.ts new file mode 100644 index 00000000..0c37093d --- /dev/null +++ b/PurchaseConnector/models/test_purchase.ts @@ -0,0 +1,13 @@ +interface TestPurchaseJson {} + +export class TestPurchase { + constructor() {} + + static fromJson(json: TestPurchaseJson): TestPurchase { + return new TestPurchase(); + } + + toJson(): TestPurchaseJson { + return {}; + } +} diff --git a/PurchaseConnector/models/validation_failure_data.ts b/PurchaseConnector/models/validation_failure_data.ts new file mode 100644 index 00000000..69b9027a --- /dev/null +++ b/PurchaseConnector/models/validation_failure_data.ts @@ -0,0 +1,25 @@ +export type ValidationFailureDataJson = { + status: number; + description: string; +}; + +export class ValidationFailureData { + status: number; + description: string; + + constructor(status: number, description: string) { + this.status = status; + this.description = description; + } + + static fromJson(json: ValidationFailureDataJson): ValidationFailureData { + return new ValidationFailureData(json.status, json.description); + } + + toJson(): ValidationFailureDataJson { + return { + status: this.status, + description: this.description, + }; + } +} diff --git a/PurchaseConnector/utils/connector_callbacks.ts b/PurchaseConnector/utils/connector_callbacks.ts new file mode 100644 index 00000000..903836b7 --- /dev/null +++ b/PurchaseConnector/utils/connector_callbacks.ts @@ -0,0 +1,14 @@ +import { IosError, JVMThrowable } from "../models"; + +// Type definition for a general-purpose listener. +export type PurchaseConnectorListener = (data: any) => void; + +// Type definition for a listener which gets called when the `PurchaseConnectorImpl` receives purchase revenue validation info for iOS. +export type DidReceivePurchaseRevenueValidationInfo = (validationInfo?: Map, error?: IosError) => void; + +// Invoked when a 200 OK response is received from the server. +// Note: An INVALID purchase is considered to be a successful response and will also be returned by this callback. +export type OnResponse = (result?: Map) => void; + +// Invoked when a network exception occurs or a non 200/OK response is received from the server. +export type OnFailure = (result: string, error?: JVMThrowable) => void; \ No newline at end of file diff --git a/android/.project b/android/.project index 3a1ae896..2efaaed1 100644 --- a/android/.project +++ b/android/.project @@ -5,11 +5,6 @@ - - org.eclipse.jdt.core.javabuilder - - - org.eclipse.buildship.core.gradleprojectbuilder @@ -17,7 +12,6 @@ - org.eclipse.jdt.core.javanature org.eclipse.buildship.core.gradleprojectnature diff --git a/android/build.gradle b/android/build.gradle index 47b22e87..a088c864 100755 --- a/android/build.gradle +++ b/android/build.gradle @@ -19,6 +19,8 @@ buildscript { apply plugin: 'com.android.library' +def includeConnector = project.hasProperty('appsflyer.enable_purchase_connector') ? project.property('appsflyer.enable_purchase_connector').toBoolean() : false // Moved outside of android block + android { compileSdkVersion safeExtGet('compileSdkVersion', 34) buildToolsVersion safeExtGet('buildToolsVersion', '34.0.0') @@ -32,6 +34,8 @@ android { abiFilters "armeabi-v7a", "x86" 00 } + + buildConfigField 'boolean', 'INCLUDE_CONNECTOR', includeConnector.toString() } lintOptions { warning 'InvalidPackage' @@ -53,6 +57,9 @@ repositories { dependencies { implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}" - implementation "com.android.installreferrer:installreferrer:${safeExtGet('installReferrerVersion', '2.1')}" api "com.appsflyer:af-android-sdk:${safeExtGet('appsflyerVersion', '6.15.1')}" + implementation "com.android.installreferrer:installreferrer:${safeExtGet('installReferrerVersion', '2.2')}" + if (includeConnector){ + implementation 'com.appsflyer:purchase-connector:2.0.1' + } } diff --git a/android/src/main/java/com/appsflyer/reactnative/PCAppsFlyerModule.java b/android/src/main/java/com/appsflyer/reactnative/PCAppsFlyerModule.java new file mode 100644 index 00000000..3d01a8f6 --- /dev/null +++ b/android/src/main/java/com/appsflyer/reactnative/PCAppsFlyerModule.java @@ -0,0 +1,443 @@ +package com.appsflyer.reactnative; + +import android.app.Application; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableType; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Iterator; +import java.util.Collections; +import java.util.stream.Collectors; +import java.util.Arrays; + +import com.appsflyer.api.PurchaseClient; +import com.appsflyer.api.Store; +import com.appsflyer.internal.models.*; +import com.appsflyer.internal.models.InAppPurchaseValidationResult; +import com.appsflyer.internal.models.SubscriptionPurchase; +import com.appsflyer.internal.models.SubscriptionValidationResult; +import com.appsflyer.internal.models.ValidationFailureData; + +import static com.appsflyer.reactnative.RNAppsFlyerConstants.*; + +interface MappedValidationResultListener extends PurchaseClient.ValidationResultListener> { + void onResponse(Map response); + + void onFailure(String result, Throwable error); +} + +public class PCAppsFlyerModule extends ReactContextBaseJavaModule { + + private ReactApplicationContext reactContext; + private Application application; + private boolean isModuleEnabled; + private ConnectorWrapper connectorWrapper; + private String TAG = "AppsFlyer_" + PLUGIN_VERSION; + + public PCAppsFlyerModule(ReactApplicationContext reactContext) { + super(reactContext); + this.reactContext = reactContext; + this.application = (Application) reactContext.getApplicationContext(); + this.isModuleEnabled = BuildConfig.INCLUDE_CONNECTOR; + } + + @Override + public String getName() { + return "PCAppsFlyer"; + } + + @ReactMethod + public void create(ReadableMap config) { + Log.d(TAG, "Attempting to create connector with config: " + config.toString()); + // Check if the module is enabled before proceeding + if (!this.isModuleEnabled) { + Log.e(TAG, "Failed to create connector: module is not enabled.\n" + ENABLE_MODULE_MESSAGE); + } + + if (this.connectorWrapper == null) { + boolean logSubscriptions = config.getBoolean("logSubscriptions"); + boolean logInApps = config.getBoolean("logInApps"); + boolean sandbox = config.getBoolean("sandbox"); + + MappedValidationResultListener arsListener = this.arsListener; + MappedValidationResultListener viapListener = this.viapListener; + + // Instantiate the ConnectorWrapper with the config parameters. + this.connectorWrapper = new ConnectorWrapper( + this.reactContext, + logSubscriptions, + logInApps, + sandbox, + arsListener, + viapListener + ); + Log.d(TAG, "The Purchase Connector initiated successfully."); + } else { + // ConnectorWrapper is already configured, log an error message. + Log.e(TAG, "The Purchase Connector is already configured and cannot be created again."); + } + } + + @ReactMethod + public void startObservingTransactions() { + if (isModuleEnabled) { + connectorWrapper.startObservingTransactions(); + Log.d(TAG, "Start Observing Transactions..."); + } else { + Log.e(TAG, "Module is not enabled.\n" + ENABLE_MODULE_MESSAGE); + } + } + + @ReactMethod + public void stopObservingTransactions() { + if (isModuleEnabled) { + connectorWrapper.stopObservingTransactions(); + Log.d(TAG, "Stopped Observing Transactions..."); + } else { + Log.e(TAG, "Module is not enabled.\n" + ENABLE_MODULE_MESSAGE); + } + } + + // Initialization of the ARSListener + private final MappedValidationResultListener arsListener = new MappedValidationResultListener() { + @Override + public void onFailure(String result, Throwable error) { + handleError(EVENT_SUBSCRIPTION_VALIDATION_RESULT, result, error); + } + + @Override + public void onResponse(Map response) { + handleSuccess(EVENT_SUBSCRIPTION_VALIDATION_RESULT, response); + } + }; + + // Initialization of the VIAPListener + private final MappedValidationResultListener viapListener = new MappedValidationResultListener() { + @Override + public void onFailure(String result, Throwable error) { + handleError(EVENT_IN_APP_PURCHASE_VALIDATION_RESULT, result, error); + } + + @Override + public void onResponse(Map response) { + handleSuccess(EVENT_IN_APP_PURCHASE_VALIDATION_RESULT, response); + } + }; + + //HELPER METHODS + private void handleSuccess(String eventName, Map response) { + emitEvent(eventName, convertMapToWritableMap(response)); + } + + private void handleError(String eventName, String result, Throwable error) { + WritableMap resMap = Arguments.createMap(); + resMap.putString("result", result); + resMap.putMap("error", error != null ? errorToMap(error) : null); + emitEvent(eventName, resMap); + } + + private void emitEvent(String eventName, WritableMap eventData) { + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, eventData); + } + + private WritableMap errorToMap(Throwable error) { + JSONObject errorJson = new JSONObject(this.toMap(error)); + WritableMap errorMap = RNUtil.jsonToWritableMap(errorJson); + return errorMap; + } + + private WritableMap convertMapToWritableMap(Map map) { + JSONObject jsonMap = new JSONObject(map); + WritableMap writableMap = RNUtil.jsonToWritableMap(jsonMap); + return writableMap; + } + + @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. + } + + private Map toMap(Throwable throwable) { + Map map = new HashMap<>(); + map.put("type", throwable.getClass().getSimpleName()); + map.put("message", throwable.getMessage()); + map.put("stacktrace", String.join("\n", Arrays.stream(throwable.getStackTrace()).map(StackTraceElement::toString).toArray(String[]::new))); + map.put("cause", throwable.getCause() != null ? toMap(throwable.getCause()) : null); + return map; + } +} + +class ConnectorWrapper implements PurchaseClient { + private final PurchaseClient connector; + + public ConnectorWrapper(Context context, boolean logSubs, boolean logInApps, boolean sandbox, + MappedValidationResultListener subsListener, MappedValidationResultListener inAppListener) { + this.connector = new PurchaseClient.Builder(context, Store.GOOGLE) + .setSandbox(sandbox) + .logSubscriptions(logSubs) + .autoLogInApps(logInApps) + .setSubscriptionValidationResultListener(new PurchaseClient.SubscriptionPurchaseValidationResultListener() { + @Override + public void onResponse(Map result) { + if (result != null) { + Map mappedResults = result.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> toJsonMap(entry.getValue()))); + subsListener.onResponse(mappedResults); + } else { + subsListener.onResponse(null); + } + } + + @Override + public void onFailure(String result, Throwable error) { + subsListener.onFailure(result, error); + } + }) + .setInAppValidationResultListener(new PurchaseClient.InAppPurchaseValidationResultListener() { + @Override + public void onResponse(Map result) { + if (result != null) { + Map mappedResults = result.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> toJsonMap(entry.getValue()))); + inAppListener.onResponse(mappedResults); + } else { + inAppListener.onResponse(null); + } + } + + @Override + public void onFailure(String result, Throwable error) { + inAppListener.onFailure(result, error); + } + }) + .build(); + } + + /** + * Starts observing all incoming transactions from the play store. + */ + @Override + public void startObservingTransactions() { + connector.startObservingTransactions(); + } + + /** + * Stops observing all incoming transactions from the play store. + */ + @Override + public void stopObservingTransactions() { + connector.stopObservingTransactions(); + } + + private Map toJsonMap(SubscriptionPurchase subscriptionPurchase) { + Map map = new HashMap<>(); + map.put("acknowledgementState", subscriptionPurchase.getAcknowledgementState()); + map.put("canceledStateContext", subscriptionPurchase.getCanceledStateContext() != null ? toJsonMap(subscriptionPurchase.getCanceledStateContext()) : null); + map.put("externalAccountIdentifiers", subscriptionPurchase.getExternalAccountIdentifiers() != null ? toJsonMap(subscriptionPurchase.getExternalAccountIdentifiers()) : null); + map.put("kind", subscriptionPurchase.getKind()); + map.put("latestOrderId", subscriptionPurchase.getLatestOrderId()); + map.put("lineItems", subscriptionPurchase.getLineItems().stream().map(this::toJsonMap).collect(Collectors.toList())); + map.put("linkedPurchaseToken", subscriptionPurchase.getLinkedPurchaseToken()); + map.put("pausedStateContext", subscriptionPurchase.getPausedStateContext() != null ? toJsonMap(subscriptionPurchase.getPausedStateContext()) : null); + map.put("regionCode", subscriptionPurchase.getRegionCode()); + map.put("startTime", subscriptionPurchase.getStartTime()); + map.put("subscribeWithGoogleInfo", subscriptionPurchase.getSubscribeWithGoogleInfo() != null ? toJsonMap(subscriptionPurchase.getSubscribeWithGoogleInfo()) : null); + map.put("subscriptionState", subscriptionPurchase.getSubscriptionState()); + map.put("testPurchase", subscriptionPurchase.getTestPurchase() != null ? toJsonMap(subscriptionPurchase.getTestPurchase()) : null); + return map; + } + + private Map toJsonMap(CanceledStateContext canceledStateContext) { + Map map = new HashMap<>(); + map.put("developerInitiatedCancellation", canceledStateContext.getDeveloperInitiatedCancellation() != null ? toJsonMap(canceledStateContext.getDeveloperInitiatedCancellation()) : null); + map.put("replacementCancellation", canceledStateContext.getReplacementCancellation() != null ? toJsonMap(canceledStateContext.getReplacementCancellation()) : null); + map.put("systemInitiatedCancellation", canceledStateContext.getSystemInitiatedCancellation() != null ? toJsonMap(canceledStateContext.getSystemInitiatedCancellation()) : null); + map.put("userInitiatedCancellation", canceledStateContext.getUserInitiatedCancellation() != null ? toJsonMap(canceledStateContext.getUserInitiatedCancellation()) : null); + return map; + } + + private Map toJsonMap(DeveloperInitiatedCancellation developerInitiatedCancellation) { + return new HashMap<>(); + } + + private Map toJsonMap(ReplacementCancellation replacementCancellation) { + return new HashMap<>(); + } + + private Map toJsonMap(SystemInitiatedCancellation systemInitiatedCancellation) { + return new HashMap<>(); + } + + private Map toJsonMap(UserInitiatedCancellation userInitiatedCancellation) { + Map map = new HashMap<>(); + map.put("cancelSurveyResult", userInitiatedCancellation.getCancelSurveyResult() != null ? toJsonMap(userInitiatedCancellation.getCancelSurveyResult()) : null); + map.put("cancelTime", userInitiatedCancellation.getCancelTime()); + return map; + } + + private Map toJsonMap(CancelSurveyResult cancelSurveyResult) { + Map map = new HashMap<>(); + map.put("reason", cancelSurveyResult.getReason()); + map.put("reasonUserInput", cancelSurveyResult.getReasonUserInput()); + return map; + } + + private Map toJsonMap(ExternalAccountIdentifiers externalAccountIdentifiers) { + Map map = new HashMap<>(); + map.put("externalAccountId", externalAccountIdentifiers.getExternalAccountId()); + map.put("obfuscatedExternalAccountId", externalAccountIdentifiers.getObfuscatedExternalAccountId()); + map.put("obfuscatedExternalProfileId", externalAccountIdentifiers.getObfuscatedExternalProfileId()); + return map; + } + + private Map toJsonMap(SubscriptionPurchaseLineItem subscriptionPurchaseLineItem) { + Map map = new HashMap<>(); + map.put("autoRenewingPlan", subscriptionPurchaseLineItem.getAutoRenewingPlan() != null ? toJsonMap(subscriptionPurchaseLineItem.getAutoRenewingPlan()) : null); + map.put("deferredItemReplacement", subscriptionPurchaseLineItem.getDeferredItemReplacement() != null ? toJsonMap(subscriptionPurchaseLineItem.getDeferredItemReplacement()) : null); + map.put("expiryTime", subscriptionPurchaseLineItem.getExpiryTime()); + map.put("offerDetails", subscriptionPurchaseLineItem.getOfferDetails() != null ? toJsonMap(subscriptionPurchaseLineItem.getOfferDetails()) : null); + map.put("prepaidPlan", subscriptionPurchaseLineItem.getPrepaidPlan() != null ? toJsonMap(subscriptionPurchaseLineItem.getPrepaidPlan()) : null); + map.put("productId", subscriptionPurchaseLineItem.getProductId()); + return map; + } + + private Map toJsonMap(OfferDetails offerDetails) { + Map map = new HashMap<>(); + map.put("offerTags", offerDetails.getOfferTags()); + map.put("basePlanId", offerDetails.getBasePlanId()); + map.put("offerId", offerDetails.getOfferId()); + return map; + } + + private Map toJsonMap(AutoRenewingPlan autoRenewingPlan) { + Map map = new HashMap<>(); + map.put("autoRenewEnabled", autoRenewingPlan.getAutoRenewEnabled()); + map.put("priceChangeDetails", autoRenewingPlan.getPriceChangeDetails() != null ? toJsonMap(autoRenewingPlan.getPriceChangeDetails()) : null); + return map; + } + + private Map toJsonMap(SubscriptionItemPriceChangeDetails subscriptionItemPriceChangeDetails) { + Map map = new HashMap<>(); + map.put("expectedNewPriceChargeTime", subscriptionItemPriceChangeDetails.getExpectedNewPriceChargeTime()); + map.put("newPrice", subscriptionItemPriceChangeDetails.getNewPrice() != null ? toJsonMap(subscriptionItemPriceChangeDetails.getNewPrice()) : null); + map.put("priceChangeMode", subscriptionItemPriceChangeDetails.getPriceChangeMode()); + map.put("priceChangeState", subscriptionItemPriceChangeDetails.getPriceChangeState()); + return map; + } + + private Map toJsonMap(Money money) { + Map map = new HashMap<>(); + map.put("currencyCode", money.getCurrencyCode()); + map.put("nanos", money.getNanos()); + map.put("units", money.getUnits()); + return map; + } + + private Map toJsonMap(DeferredItemReplacement deferredItemReplacement) { + Map map = new HashMap<>(); + map.put("productId", deferredItemReplacement.getProductId()); + return map; + } + + private Map toJsonMap(PrepaidPlan prepaidPlan) { + Map map = new HashMap<>(); + map.put("allowExtendAfterTime", prepaidPlan.getAllowExtendAfterTime()); + return map; + } + + private Map toJsonMap(PausedStateContext pausedStateContext) { + Map map = new HashMap<>(); + map.put("autoResumeTime", pausedStateContext.getAutoResumeTime()); + return map; + } + + private Map toJsonMap(SubscribeWithGoogleInfo subscribeWithGoogleInfo) { + Map map = new HashMap<>(); + map.put("emailAddress", subscribeWithGoogleInfo.getEmailAddress()); + map.put("familyName", subscribeWithGoogleInfo.getFamilyName()); + map.put("givenName", subscribeWithGoogleInfo.getGivenName()); + map.put("profileId", subscribeWithGoogleInfo.getProfileId()); + map.put("profileName", subscribeWithGoogleInfo.getProfileName()); + return map; + } + + public Map toJsonMap(TestPurchase testPurchase) { + return new HashMap<>(); + } + + private Map toJsonMap(ProductPurchase productPurchase) { + Map map = new HashMap<>(); + map.put("kind", productPurchase.getKind()); + map.put("purchaseTimeMillis", productPurchase.getPurchaseTimeMillis()); + map.put("purchaseState", productPurchase.getPurchaseState()); + map.put("consumptionState", productPurchase.getConsumptionState()); + map.put("developerPayload", productPurchase.getDeveloperPayload()); + map.put("orderId", productPurchase.getOrderId()); + map.put("purchaseType", productPurchase.getPurchaseType()); + map.put("acknowledgementState", productPurchase.getAcknowledgementState()); + map.put("purchaseToken", productPurchase.getPurchaseToken()); + map.put("productId", productPurchase.getProductId()); + map.put("quantity", productPurchase.getQuantity()); + map.put("obfuscatedExternalAccountId", productPurchase.getObfuscatedExternalAccountId()); + map.put("obfuscatedExternalProfileId", productPurchase.getObfuscatedExternalProfileId()); + map.put("regionCode", productPurchase.getRegionCode()); + return map; + } + + private Map toJsonMap(InAppPurchaseValidationResult inAppPurchaseValidationResult) { + Map map = new HashMap<>(); + map.put("success", inAppPurchaseValidationResult.getSuccess()); + map.put("productPurchase", inAppPurchaseValidationResult.getProductPurchase() != null ? toJsonMap(inAppPurchaseValidationResult.getProductPurchase()) : null); + map.put("failureData", inAppPurchaseValidationResult.getFailureData() != null ? toJsonMap(inAppPurchaseValidationResult.getFailureData()) : null); + return map; + } + + private Map toJsonMap(SubscriptionValidationResult subscriptionValidationResult) { + Map map = new HashMap<>(); + map.put("success", subscriptionValidationResult.getSuccess()); + map.put("subscriptionPurchase", subscriptionValidationResult.getSubscriptionPurchase() != null ? toJsonMap(subscriptionValidationResult.getSubscriptionPurchase()) : null); + map.put("failureData", subscriptionValidationResult.getFailureData() != null ? toJsonMap(subscriptionValidationResult.getFailureData()) : null); + return map; + } + + private Map toJsonMap(ValidationFailureData validationFailureData) { + Map map = new HashMap<>(); + map.put("status", validationFailureData.getStatus()); + map.put("description", validationFailureData.getDescription()); + return map; + } +} \ No newline at end of file diff --git a/android/src/main/java/com/appsflyer/reactnative/PCAppsFlyerPackage.java b/android/src/main/java/com/appsflyer/reactnative/PCAppsFlyerPackage.java new file mode 100644 index 00000000..18040c72 --- /dev/null +++ b/android/src/main/java/com/appsflyer/reactnative/PCAppsFlyerPackage.java @@ -0,0 +1,32 @@ +package com.appsflyer.reactnative; + +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.uimanager.ViewManager; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class PCAppsFlyerPackage implements ReactPackage { + + public PCAppsFlyerPackage() { + } + + + public List> createJSModules() { + return Collections.emptyList(); + } + + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + return Arrays.asList(new PCAppsFlyerModule(reactContext)); + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java b/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java index 4e88d25f..21aff74c 100755 --- a/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java +++ b/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java @@ -58,5 +58,9 @@ public class RNAppsFlyerConstants { final static String AF_MEDIATION_NETWORK = "mediationNetwork"; final static String AF_ADDITIONAL_PARAMETERS = "additionalParameters"; + //Purchase Connector + final static String EVENT_SUBSCRIPTION_VALIDATION_RESULT = "onSubscriptionValidationResult"; + final static String EVENT_IN_APP_PURCHASE_VALIDATION_RESULT = "onInAppPurchaseValidationResult"; + final static String ENABLE_MODULE_MESSAGE = "Please set appsflyer.enable_purchase_connector to true in your gradle.properties file."; } diff --git a/demos/appsflyer-react-native-app/android/app/build.gradle b/demos/appsflyer-react-native-app/android/app/build.gradle index 58aa614a..7ce0d22f 100644 --- a/demos/appsflyer-react-native-app/android/app/build.gradle +++ b/demos/appsflyer-react-native-app/android/app/build.gradle @@ -179,6 +179,8 @@ android { } dependencies { + implementation "com.android.billingclient:billing:7.0.0" + implementation fileTree(dir: "libs", include: ["*.jar"]) //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:+" // From node_modules diff --git a/demos/appsflyer-react-native-app/android/gradle.properties b/demos/appsflyer-react-native-app/android/gradle.properties index f1419d1e..44e6a675 100644 --- a/demos/appsflyer-react-native-app/android/gradle.properties +++ b/demos/appsflyer-react-native-app/android/gradle.properties @@ -26,3 +26,4 @@ android.enableJetifier=true # Version of flipper SDK to use with React Native FLIPPER_VERSION=0.93.0 +appsflyer.enable_purchase_connector=true diff --git a/demos/appsflyer-react-native-app/components/AppsFlyer.js b/demos/appsflyer-react-native-app/components/AppsFlyer.js index 683a8ecf..a6b992e6 100644 --- a/demos/appsflyer-react-native-app/components/AppsFlyer.js +++ b/demos/appsflyer-react-native-app/components/AppsFlyer.js @@ -1,4 +1,8 @@ -import appsFlyer , {MEDIATION_NETWORK} from 'react-native-appsflyer'; +import appsFlyer, { + AppsFlyerPurchaseConnector, + AppsFlyerPurchaseConnectorConfig, + MEDIATION_NETWORK, +} from 'react-native-appsflyer'; import {Platform} from 'react-native'; // events @@ -9,40 +13,60 @@ export const AF_checkout = 'af_check_out'; export const AF_clickOnItem = 'af_click_on_item'; const initOptions = { - isDebug: true, - devKey: 'Us4GmXxXx46Qed', - onInstallConversionDataListener: true, - timeToWaitForATTUserAuthorization: 10, - onDeepLinkListener: true, - appId: '741993747' + isDebug: true, + devKey: 'Us4GmXxXx46Qed', + onInstallConversionDataListener: true, + timeToWaitForATTUserAuthorization: 10, + onDeepLinkListener: true, + appId: '741993747', }; // AppsFlyer initialization flow. ends with initSdk. export function AFInit() { if (Platform.OS == 'ios') { - appsFlyer.setCurrentDeviceLanguage("EN"); + appsFlyer.setCurrentDeviceLanguage('EN'); } appsFlyer.setAppInviteOneLinkID('oW4R'); appsFlyer.initSdk(initOptions, null, null); + + +} + +export function PCInit() { + const purchaseConnectorConfig: PurchaseConnectorConfig = AppsFlyerPurchaseConnectorConfig.setConfig({ + logSubscriptions: true, + logInApps: true, + sandbox: true, + }); + + AppsFlyerPurchaseConnector.create( + purchaseConnectorConfig, + ); + AppsFlyerPurchaseConnector.startObservingTransactions(); } // Sends in-app events to AppsFlyer servers. name is the events name ('simple event') and the values are a JSON ({info: 'fff', size: 5}) export function AFLogEvent(name, values) { - appsFlyer.logEvent(name, values, null, null); + appsFlyer.logEvent(name, values,(res) => { + console.log(res); + }, + (err) => { + console.log(err); + }); AFLogAdRevenue(); } -export function AFLogAdRevenue(){ +export function AFLogAdRevenue() { const adRevenueData = { monetizationNetwork: 'AF-AdNetwork', mediationNetwork: MEDIATION_NETWORK.IRONSOURCE, currencyIso4217Code: 'USD', - revenue: 1.23, - additionalParameters : { + revenue: 1.23, + additionalParameters: { customParam1: 'value1', customParam2: 'value2', - } - }; + }, + }; appsFlyer.logAdRevenue(adRevenueData); } diff --git a/demos/appsflyer-react-native-app/components/HomeScreen.js b/demos/appsflyer-react-native-app/components/HomeScreen.js index 6555b857..db29853f 100644 --- a/demos/appsflyer-react-native-app/components/HomeScreen.js +++ b/demos/appsflyer-react-native-app/components/HomeScreen.js @@ -14,6 +14,7 @@ import {Card, ListItem, Button, FAB, Badge} from 'react-native-elements'; import Icon from 'react-native-vector-icons/FontAwesome'; import appsFlyer from 'react-native-appsflyer'; import { + PCInit, AFInit, AFLogEvent, AF_clickOnItem, @@ -162,6 +163,7 @@ const HomeScreen = ({navigation}) => { } }); AFInit(); + PCInit(); return () => { AFGCDListener(); diff --git a/demos/appsflyer-react-native-app/ios/Podfile b/demos/appsflyer-react-native-app/ios/Podfile index 85010b34..610439a0 100644 --- a/demos/appsflyer-react-native-app/ios/Podfile +++ b/demos/appsflyer-react-native-app/ios/Podfile @@ -3,6 +3,9 @@ require_relative '../node_modules/@react-native-community/cli-platform-ios/nativ platform :ios, '13.0' +# Uncomment the following for using PurchaseConnector +$AppsFlyerPurchaseConnector = true + target 'AppsFlyerExample' do config = use_native_modules! diff --git a/demos/appsflyer-react-native-app/metro.config.js b/demos/appsflyer-react-native-app/metro.config.js index e91aba93..26a05589 100644 --- a/demos/appsflyer-react-native-app/metro.config.js +++ b/demos/appsflyer-react-native-app/metro.config.js @@ -5,6 +5,10 @@ * @format */ +const localPackagePaths = [ + '/Users/amit.levy/RN-Projects/appsflyer-react-native-plugin', +]; + module.exports = { transformer: { getTransformOptions: async () => ({ @@ -14,4 +18,8 @@ module.exports = { }, }), }, + resolver: { + nodeModulesPaths: [...localPackagePaths], // update to resolver + }, + watchFolders: [...localPackagePaths], // update to watch }; diff --git a/demos/appsflyer-react-native-app/package.json b/demos/appsflyer-react-native-app/package.json index ad68af26..5c95132f 100644 --- a/demos/appsflyer-react-native-app/package.json +++ b/demos/appsflyer-react-native-app/package.json @@ -14,7 +14,7 @@ "@react-navigation/stack": "^6.0.7", "react": "18.1.0", "react-native": "0.70.6", - "react-native-appsflyer": "../../", + "react-native-appsflyer": "file:../../", "react-native-elements": "^3.4.2", "react-native-gesture-handler": "^1.10.3", "react-native-safe-area-context": "^3.3.2", diff --git a/index.d.ts b/index.d.ts index 0345af26..5185cac3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,214 +1,323 @@ /** * Typescript Definition Sync with v5.1.1 **/ +import { InAppPurchaseValidationResult } from "../models/in_app_purchase_validation_result"; +import SubscriptionValidationResult from "../models/subscription_validation_result"; +import { + OnResponse, + OnFailure, + DidReceivePurchaseRevenueValidationInfo, +} from "../utils/connector_callbacks"; + declare module "react-native-appsflyer" { - type Response = void | Promise; - type SuccessCB = (result?: unknown) => unknown; - type ErrorCB = (error?: Error) => unknown; - export type ConversionData = { - status: "success" | "failure", - type: "onInstallConversionDataLoaded" | "onInstallConversionFailure" - data: { - is_first_launch: "true" | "false"; - media_source: string; - campaign: string; - af_status: "Organic" | "Non-organic"; - [key: string]: any; - } - } - - export type OnAppOpenAttributionData = { - status: "success" | "failure", - type: "onAppOpenAttribution" | "onAttributionFailure" - data: { - af_dp?: string; - is_retargeting?: string, - af_channel?: string, - af_cost_currency?: string, - c?: string; - af_adset?: string, - af_click_lookback: string, - deep_link_sub1?: string; - campaign: string; - deep_link_value: string; - link: string; - media_source: string; - pid?: string; - path?: string // Uri-Scheme - host?: string // Uri-Scheme - shortlink?: string // Uri-Scheme - scheme?: string // Uri-Scheme - af_sub1?: string; - af_sub2?: string; - af_sub3?: string; - af_sub4?: string; - af_sub5?: string; - [key: string]: any; - } - } - - export type UnifiedDeepLinkData = { - status: "success" | "failure", - type: "onDeepLinking", - deepLinkStatus: 'FOUND' | 'NOT_FOUND' | 'Error', - isDeferred: boolean, - data: { - campaign: string; - deep_link_value: string; - deep_link_sub1?: string; - media_source: string; - pid?: string; - link: string, - af_sub1?: string; - af_sub2?: string; - af_sub3?: string; - af_sub4?: string; - af_sub5?: string; - [key: string]: any; - } - } - - export enum AF_EMAIL_CRYPT_TYPE { - NONE, - SHA256 - } - - export interface InitSDKOptions { - devKey: string; - appId?: string; // iOS only - isDebug?: boolean; - onInstallConversionDataListener?: boolean; - onDeepLinkListener?: boolean; - timeToWaitForATTUserAuthorization?: number; // iOS only - manualStart?: boolean - } - - export interface InAppPurchase { - publicKey: string; - productIdentifier: string; - signature: string; - transactionId: string; - purchaseData: string; - price: string; - currency: string; - additionalParameters?: object; - } - - export interface SetEmailsOptions { - emails?: string[]; - emailsCryptType: AF_EMAIL_CRYPT_TYPE | 0 | 3; - } - - export interface GenerateInviteLinkParams { - channel: string; - campaign?: string; - customerID?: string; - userParams?: { - deep_link_value?: string; - [key: string]: any; - }; - referrerName?: string; - referrerImageUrl?: string; - deeplinkPath?: string; - baseDeeplink?: string; - brandDomain?: string; - } - - export const AppsFlyerConsent: { - forGDPRUser: (hasConsentForDataUsage: boolean, hasConsentForAdsPersonalization: boolean) => AppsFlyerConsentType; - forNonGDPRUser: () => AppsFlyerConsentType; - } - - export interface AppsFlyerConsentType { - isUserSubjectToGDPR: boolean; - hasConsentForDataUsage?: boolean; - hasConsentForAdsPersonalization?: boolean; - } - - //Log Ad Revenue Section - export enum MEDIATION_NETWORK { - IRONSOURCE , - APPLOVIN_MAX , - GOOGLE_ADMOB , - FYBER , - APPODEAL , - ADMOST , - TOPON , - TRADPLUS, - YANDEX , - CHARTBOOST , - UNITY , - TOPON_PTE , - CUSTOM_MEDIATION , - DIRECT_MONETIZATION_NETWORK - } - - //Interface representing ad revenue information - export interface AFAdRevenueData { - monetizationNetwork: string; - mediationNetwork: MEDIATION_NETWORK; - currencyIso4217Code: string; - revenue: number; - additionalParameters?: StringMap; - } - - const appsFlyer: { - onInstallConversionData(callback: (data: ConversionData) => any): () => void; - onInstallConversionFailure(callback: (data: ConversionData) => any): () => void; - onAppOpenAttribution(callback: (data: OnAppOpenAttributionData) => any): () => void; - onAttributionFailure(callback: (data: OnAppOpenAttributionData) => any): () => void; - onDeepLink(callback: (data: UnifiedDeepLinkData) => any): () => void; - initSdk(options: InitSDKOptions): Promise; - initSdk(options: InitSDKOptions, successC: SuccessCB, errorC: ErrorCB): void; - logEvent(eventName: string, eventValues: object): Promise; - logEvent(eventName: string, eventValues: object, successC: SuccessCB, errorC: ErrorCB): void; - setUserEmails(options: SetEmailsOptions, successC: SuccessCB, errorC: ErrorCB): void - setAdditionalData(additionalData: object, successC?: SuccessCB): void - getAppsFlyerUID(callback: (error: Error, uid: string) => any): void - setCustomerUserId(userId: string, successC?: SuccessCB): void - stop(isStopped: boolean, successC?: SuccessCB): void - setAppInviteOneLinkID(oneLinkID: string, successC?: SuccessCB): void - generateInviteLink(params: GenerateInviteLinkParams, successC: SuccessCB, errorC: ErrorCB): void - logCrossPromotionImpression(appId: string, campaign: string, parameters: object): void - logCrossPromotionAndOpenStore(appId: string, campaign: string, params: object): void - setCurrencyCode(currencyCode: string, successC?: SuccessCB): void - anonymizeUser(shouldAnonymize: boolean, successC?: SuccessCB): void - setOneLinkCustomDomains(domains: string[], successC: SuccessCB, errorC: ErrorCB): void - setResolveDeepLinkURLs(urls: string[], successC: SuccessCB, errorC: ErrorCB): void - performOnAppAttribution(urlString: string, successC: SuccessCB, errorC: ErrorCB): void - setSharingFilterForAllPartners(): void - setSharingFilter(partners: string[], successC?: SuccessCB, errorC?: ErrorCB): void - logLocation(longitude: number, latitude: number, successC?: SuccessCB): void - validateAndLogInAppPurchase(purchaseInfo: InAppPurchase, successC: SuccessCB, errorC: ErrorCB): Response - updateServerUninstallToken(token: string, successC?: SuccessCB): void - sendPushNotificationData(pushPayload: object, errorC?: ErrorCB): void - setHost(hostPrefix: string, hostName: string, success: SuccessCB): void - addPushNotificationDeepLinkPath(path: string[], successC: SuccessCB, errorC: ErrorCB): void - disableAdvertisingIdentifier(isDisable: boolean): void - setSharingFilterForPartners(partners: string[]): void - setPartnerData(partnerId: string, partnerData: object): void - appendParametersToDeepLinkingURL(contains: string, parameters: object): void - startSdk(): void - enableTCFDataCollection(enabled: boolean): void - setConsentData(consentData: AppsFlyerConsentType): void - logAdRevenue(adRevenueData: AFAdRevenueData) : void - /** - * For iOS Only - * */ - disableCollectASA(shouldDisable: boolean): void - setUseReceiptValidationSandbox(isSandbox: boolean): void - disableSKAD(disableSkad: boolean): void - setCurrentDeviceLanguage(language: string): void - disableIDFVCollection(shouldDisable: boolean): void - - /** - * For Android Only - * */ - setCollectIMEI(isCollect: boolean, successC?: SuccessCB): void - setCollectAndroidID(isCollect: boolean, successC?: SuccessCB): void - setDisableNetworkData(disable: boolean): void - performOnDeepLinking(): void + type Response = void | Promise; + type SuccessCB = (result?: unknown) => unknown; + type ErrorCB = (error?: Error) => unknown; + export type ConversionData = { + status: "success" | "failure"; + type: "onInstallConversionDataLoaded" | "onInstallConversionFailure"; + data: { + is_first_launch: "true" | "false"; + media_source: string; + campaign: string; + af_status: "Organic" | "Non-organic"; + [key: string]: any; + }; + }; + + export type OnAppOpenAttributionData = { + status: "success" | "failure"; + type: "onAppOpenAttribution" | "onAttributionFailure"; + data: { + af_dp?: string; + is_retargeting?: string; + af_channel?: string; + af_cost_currency?: string; + c?: string; + af_adset?: string; + af_click_lookback: string; + deep_link_sub1?: string; + campaign: string; + deep_link_value: string; + link: string; + media_source: string; + pid?: string; + path?: string; // Uri-Scheme + host?: string; // Uri-Scheme + shortlink?: string; // Uri-Scheme + scheme?: string; // Uri-Scheme + af_sub1?: string; + af_sub2?: string; + af_sub3?: string; + af_sub4?: string; + af_sub5?: string; + [key: string]: any; + }; + }; + + export type UnifiedDeepLinkData = { + status: "success" | "failure"; + type: "onDeepLinking"; + deepLinkStatus: "FOUND" | "NOT_FOUND" | "Error"; + isDeferred: boolean; + data: { + campaign: string; + deep_link_value: string; + deep_link_sub1?: string; + media_source: string; + pid?: string; + link: string; + af_sub1?: string; + af_sub2?: string; + af_sub3?: string; + af_sub4?: string; + af_sub5?: string; + [key: string]: any; + }; + }; + + export enum AF_EMAIL_CRYPT_TYPE { + NONE, + SHA256, + } + + export interface InitSDKOptions { + devKey: string; + appId?: string; // iOS only + isDebug?: boolean; + onInstallConversionDataListener?: boolean; + onDeepLinkListener?: boolean; + timeToWaitForATTUserAuthorization?: number; // iOS only + manualStart?: boolean; + } + + export interface InAppPurchase { + publicKey: string; + productIdentifier: string; + signature: string; + transactionId: string; + purchaseData: string; + price: string; + currency: string; + additionalParameters?: object; + } + + export interface SetEmailsOptions { + emails?: string[]; + emailsCryptType: AF_EMAIL_CRYPT_TYPE | 0 | 3; + } + + export interface GenerateInviteLinkParams { + channel: string; + campaign?: string; + customerID?: string; + userParams?: { + deep_link_value?: string; + [key: string]: any; }; + referrerName?: string; + referrerImageUrl?: string; + deeplinkPath?: string; + baseDeeplink?: string; + brandDomain?: string; + } + + export const AppsFlyerConsent: { + forGDPRUser: ( + hasConsentForDataUsage: boolean, + hasConsentForAdsPersonalization: boolean + ) => AppsFlyerConsentType; + forNonGDPRUser: () => AppsFlyerConsentType; + }; + + export interface AppsFlyerConsentType { + isUserSubjectToGDPR: boolean; + hasConsentForDataUsage?: boolean; + hasConsentForAdsPersonalization?: boolean; + } + + //Log Ad Revenue Section + export enum MEDIATION_NETWORK { + IRONSOURCE, + APPLOVIN_MAX, + GOOGLE_ADMOB, + FYBER, + APPODEAL, + ADMOST, + TOPON, + TRADPLUS, + YANDEX, + CHARTBOOST, + UNITY, + TOPON_PTE, + CUSTOM_MEDIATION, + DIRECT_MONETIZATION_NETWORK, + } + + //Interface representing ad revenue information + export interface AFAdRevenueData { + monetizationNetwork: string; + mediationNetwork: MEDIATION_NETWORK; + currencyIso4217Code: string; + revenue: number; + additionalParameters?: StringMap; + } + + /** + * PurchaseConnector + */ + export interface PurchaseConnectorConfig { + logSubscriptions: boolean; + logInApps: boolean; + sandbox: boolean; + } + + export const AppsFlyerPurchaseConnectorConfig: { + setConfig(config: PurchaseConnectorConfig): PurchaseConnectorConfig; + }; + + export interface PurchaseConnector { + create(config: PurchaseConnectorConfig): void; + startObservingTransactions(): void; + stopObservingTransactions(): void; + setSubscriptionValidationResultListener( + onResponse: OnResponse, + onFailure: OnFailure + ): void; + setInAppValidationResultListener( + onResponse: OnResponse, + onFailure: OnFailure + ): void; + setDidReceivePurchaseRevenueValidationInfo( + callback: DidReceivePurchaseRevenueValidationInfo + ): void; + } + + export const AppsFlyerPurchaseConnector: PurchaseConnector; + + /**********************************/ + + const appsFlyer: { + onInstallConversionData( + callback: (data: ConversionData) => any + ): () => void; + onInstallConversionFailure( + callback: (data: ConversionData) => any + ): () => void; + onAppOpenAttribution( + callback: (data: OnAppOpenAttributionData) => any + ): () => void; + onAttributionFailure( + callback: (data: OnAppOpenAttributionData) => any + ): () => void; + onDeepLink(callback: (data: UnifiedDeepLinkData) => any): () => void; + initSdk(options: InitSDKOptions): Promise; + initSdk( + options: InitSDKOptions, + successC: SuccessCB, + errorC: ErrorCB + ): void; + logEvent(eventName: string, eventValues: object): Promise; + logEvent( + eventName: string, + eventValues: object, + successC: SuccessCB, + errorC: ErrorCB + ): void; + setUserEmails( + options: SetEmailsOptions, + successC: SuccessCB, + errorC: ErrorCB + ): void; + setAdditionalData(additionalData: object, successC?: SuccessCB): void; + getAppsFlyerUID(callback: (error: Error, uid: string) => any): void; + setCustomerUserId(userId: string, successC?: SuccessCB): void; + stop(isStopped: boolean, successC?: SuccessCB): void; + setAppInviteOneLinkID(oneLinkID: string, successC?: SuccessCB): void; + generateInviteLink( + params: GenerateInviteLinkParams, + successC: SuccessCB, + errorC: ErrorCB + ): void; + logCrossPromotionImpression( + appId: string, + campaign: string, + parameters: object + ): void; + logCrossPromotionAndOpenStore( + appId: string, + campaign: string, + params: object + ): void; + setCurrencyCode(currencyCode: string, successC?: SuccessCB): void; + anonymizeUser(shouldAnonymize: boolean, successC?: SuccessCB): void; + setOneLinkCustomDomains( + domains: string[], + successC: SuccessCB, + errorC: ErrorCB + ): void; + setResolveDeepLinkURLs( + urls: string[], + successC: SuccessCB, + errorC: ErrorCB + ): void; + performOnAppAttribution( + urlString: string, + successC: SuccessCB, + errorC: ErrorCB + ): void; + setSharingFilterForAllPartners(): void; + setSharingFilter( + partners: string[], + successC?: SuccessCB, + errorC?: ErrorCB + ): void; + logLocation( + longitude: number, + latitude: number, + successC?: SuccessCB + ): void; + validateAndLogInAppPurchase( + purchaseInfo: InAppPurchase, + successC: SuccessCB, + errorC: ErrorCB + ): Response; + updateServerUninstallToken(token: string, successC?: SuccessCB): void; + sendPushNotificationData(pushPayload: object, errorC?: ErrorCB): void; + setHost(hostPrefix: string, hostName: string, success: SuccessCB): void; + addPushNotificationDeepLinkPath( + path: string[], + successC: SuccessCB, + errorC: ErrorCB + ): void; + disableAdvertisingIdentifier(isDisable: boolean): void; + setSharingFilterForPartners(partners: string[]): void; + setPartnerData(partnerId: string, partnerData: object): void; + appendParametersToDeepLinkingURL( + contains: string, + parameters: object + ): void; + startSdk(): void; + enableTCFDataCollection(enabled: boolean): void; + setConsentData(consentData: AppsFlyerConsentType): void; + logAdRevenue(adRevenueData: AFAdRevenueData): void; + /** + * For iOS Only + * */ + disableCollectASA(shouldDisable: boolean): void; + setUseReceiptValidationSandbox(isSandbox: boolean): void; + disableSKAD(disableSkad: boolean): void; + setCurrentDeviceLanguage(language: string): void; + disableIDFVCollection(shouldDisable: boolean): void; + + /** + * For Android Only + * */ + setCollectIMEI(isCollect: boolean, successC?: SuccessCB): void; + setCollectAndroidID(isCollect: boolean, successC?: SuccessCB): void; + setDisableNetworkData(disable: boolean): void; + performOnDeepLinking(): void; + }; - export default appsFlyer; + export default appsFlyer; } diff --git a/index.js b/index.js index 7a4d3797..44fdac3f 100755 --- a/index.js +++ b/index.js @@ -1,85 +1,213 @@ -import { NativeEventEmitter, NativeModules } from 'react-native'; +import { NativeEventEmitter, NativeModules } from "react-native"; const { RNAppsFlyer } = NativeModules; const appsFlyer = {}; const eventsMap = {}; const appsFlyerEventEmitter = new NativeEventEmitter(RNAppsFlyer); +//Purchase Connector native bridge objects +const { PCAppsFlyer } = NativeModules; +const AppsFlyerPurchaseConnector = {}; +const pcEventsMap = {}; +const purchaseConnectorEventEmitter = new NativeEventEmitter( + AppsFlyerPurchaseConnector +); + +function startObservingTransactions() { + PCAppsFlyer.startObservingTransactions(); +} + +AppsFlyerPurchaseConnector.startObservingTransactions = + startObservingTransactions; + +function stopObservingTransactions() { + PCAppsFlyer.stopObservingTransactions(); +} + +AppsFlyerPurchaseConnector.stopObservingTransactions = + stopObservingTransactions; + +function setSubscriptionValidationResultListener(onResponse, onFailure) { + const subValidationListener = purchaseConnectorEventEmitter.addListener( + AppsFlyerConstants.SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER, + (result) => { + try { + const validationResult = JSON.parse(result); + onResponse(validationResult); + } catch (error) { + onFailure("Failed to parse subscription validation result", error); + } + } + ); + + pcEventsMap[ + AppsFlyerConstants.SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER + ] = subValidationListener; + + return function remove() { + subValidationListener.remove(); + }; +} + +AppsFlyerPurchaseConnector.setSubscriptionValidationResultListener = + setSubscriptionValidationResultListener; + +function setInAppValidationResultListener(onResponse, onFailure) { + const inAppValidationListener = purchaseConnectorEventEmitter.addListener( + AppsFlyerConstants.IN_APP_VALIDATION_RESULT_LISTENER, + (result) => { + try { + const validationResult = JSON.parse(result); + onResponse(validationResult); + } catch (error) { + onFailure("Failed to parse in-app purchase validation result", error); + } + } + ); + + pcEventsMap[AppsFlyerConstants.IN_APP_VALIDATION_RESULT_LISTENER] = + inAppValidationListener; + + return function remove() { + inAppValidationListener.remove(); + }; +} + +AppsFlyerPurchaseConnector.setInAppValidationResultListener = + setInAppValidationResultListener; + +function setDidReceivePurchaseRevenueValidationInfo(callback) { + const revenueValidationListener = purchaseConnectorEventEmitter.addListener( + AppsFlyerConstants.DID_RECEIVE_PURCHASE_REVENUE_VALIDATION_INFO, + (info) => { + try { + const validationInfo = JSON.parse(info); + callback(validationInfo); + } catch (error) { + callback(null, error); + } + } + ); + + pcEventsMap[AppsFlyerConstants.DID_RECEIVE_PURCHASE_REVENUE_VALIDATION_INFO] = + revenueValidationListener; + + return function remove() { + revenueValidationListener.remove(); + }; +} + +AppsFlyerPurchaseConnector.setDidReceivePurchaseRevenueValidationInfo = + setDidReceivePurchaseRevenueValidationInfo; + +const AppsFlyerPurchaseConnectorConfig = { + setConfig: ({ logSubscriptions, logInApps, sandbox }) => { + return { + logSubscriptions, + logInApps, + sandbox, + }; + }, +}; + +function create(config) { + if (!config) { + throw new MissingConfigurationException(); + } + PCAppsFlyer.create(config); +} + +AppsFlyerPurchaseConnector.create = create; + +export { AppsFlyerPurchaseConnector, AppsFlyerPurchaseConnectorConfig }; +/********************************************************/ + function initSdkCallback(options, successC, errorC) { - if (typeof options.appId !== 'string' && typeof options.appId !== 'undefined') { - return errorC('appId should be a string!'); - } - if (typeof options.isDebug !== 'boolean' && typeof options.isDebug !== 'undefined') { - return errorC('isDebug should be a boolean!'); - } - return RNAppsFlyer.initSdkWithCallBack(options, successC, errorC); + if ( + typeof options.appId !== "string" && + typeof options.appId !== "undefined" + ) { + return errorC("appId should be a string!"); + } + if ( + typeof options.isDebug !== "boolean" && + typeof options.isDebug !== "undefined" + ) { + return errorC("isDebug should be a boolean!"); + } + return RNAppsFlyer.initSdkWithCallBack(options, successC, errorC); } function initSdkPromise(options): Promise { - if (typeof options.appId !== 'string' && typeof options.appId !== 'undefined') { - return Promise.reject('appId should be a string!'); - } - if (typeof options.isDebug !== 'boolean' && typeof options.isDebug !== 'undefined') { - return Promise.reject('isDebug should be a boolean!'); - } - return RNAppsFlyer.initSdkWithPromise(options); + if ( + typeof options.appId !== "string" && + typeof options.appId !== "undefined" + ) { + return Promise.reject("appId should be a string!"); + } + if ( + typeof options.isDebug !== "boolean" && + typeof options.isDebug !== "undefined" + ) { + return Promise.reject("isDebug should be a boolean!"); + } + return RNAppsFlyer.initSdkWithPromise(options); } function initSdk(options, success, error): Promise { - if (success && error) { - //initSdk is a callback function - initSdkCallback(options, success, error); - } else if (!success) { - //initSdk is a promise function - return initSdkPromise(options); - } + if (success && error) { + //initSdk is a callback function + initSdkCallback(options, success, error); + } else if (!success) { + //initSdk is a promise function + return initSdkPromise(options); + } } appsFlyer.initSdk = initSdk; function logEventCallback(eventName, eventValues, successC, errorC) { - return RNAppsFlyer.logEvent(eventName, eventValues, successC, errorC); + return RNAppsFlyer.logEvent(eventName, eventValues, successC, errorC); } function logEventPromise(eventName, eventValues): Promise { - return RNAppsFlyer.logEventWithPromise(eventName, eventValues); + return RNAppsFlyer.logEventWithPromise(eventName, eventValues); } function logEvent(eventName, eventValues, success, error): Promise { - if (success && error) { - //logEvent is a callback function - logEventCallback(eventName, eventValues, success, error); - } else if (!success) { - // logEvent is a promise function - return logEventPromise(eventName, eventValues); - } + if (success && error) { + //logEvent is a callback function + logEventCallback(eventName, eventValues, success, error); + } else if (!success) { + // logEvent is a promise function + return logEventPromise(eventName, eventValues); + } } appsFlyer.logEvent = logEvent; - export const MEDIATION_NETWORK = Object.freeze({ - IRONSOURCE : "ironsource", - APPLOVIN_MAX : "applovinmax", - GOOGLE_ADMOB : "googleadmob", - FYBER : "fyber", - APPODEAL : "appodeal", - ADMOST : "Admost", - TOPON : "Topon", - TRADPLUS : "Tradplus", - YANDEX : "Yandex", - CHARTBOOST : "chartboost", - UNITY : "Unity", - TOPON_PTE : "toponpte", - CUSTOM_MEDIATION : "customMediation", - DIRECT_MONETIZATION_NETWORK : "directMonetizationNetwork" + IRONSOURCE: "ironsource", + APPLOVIN_MAX: "applovinmax", + GOOGLE_ADMOB: "googleadmob", + FYBER: "fyber", + APPODEAL: "appodeal", + ADMOST: "Admost", + TOPON: "Topon", + TRADPLUS: "Tradplus", + YANDEX: "Yandex", + CHARTBOOST: "chartboost", + UNITY: "Unity", + TOPON_PTE: "toponpte", + CUSTOM_MEDIATION: "customMediation", + DIRECT_MONETIZATION_NETWORK: "directMonetizationNetwork", }); function logAdRevenue(adRevenueData) { - RNAppsFlyer.logAdRevenue(adRevenueData); + RNAppsFlyer.logAdRevenue(adRevenueData); } -appsFlyer.logAdRevenue = logAdRevenue +appsFlyer.logAdRevenue = logAdRevenue; /** * Manually record the location of the user @@ -89,19 +217,26 @@ appsFlyer.logAdRevenue = logAdRevenue * @param callback success callback function */ appsFlyer.logLocation = (longitude, latitude, callback) => { - if (longitude == null || latitude == null || longitude == '' || latitude == '') { - console.log('longitude or latitude are missing!'); - return; - } - if (typeof longitude != 'number' || typeof latitude != 'number') { - longitude = parseFloat(longitude); - latitude = parseFloat(latitude); - } - if (callback) { - return RNAppsFlyer.logLocation(longitude, latitude, callback); - } else { - return RNAppsFlyer.logLocation(longitude, latitude, (result) => console.log(result)); - } + if ( + longitude == null || + latitude == null || + longitude == "" || + latitude == "" + ) { + console.log("longitude or latitude are missing!"); + return; + } + if (typeof longitude != "number" || typeof latitude != "number") { + longitude = parseFloat(longitude); + latitude = parseFloat(latitude); + } + if (callback) { + return RNAppsFlyer.logLocation(longitude, latitude, callback); + } else { + return RNAppsFlyer.logLocation(longitude, latitude, (result) => + console.log(result) + ); + } }; /** @@ -112,7 +247,7 @@ appsFlyer.logLocation = (longitude, latitude, callback) => { * @param errorC error callback function. */ appsFlyer.setUserEmails = (options, successC, errorC) => { - return RNAppsFlyer.setUserEmails(options, successC, errorC); + return RNAppsFlyer.setUserEmails(options, successC, errorC); }; /** @@ -122,11 +257,13 @@ appsFlyer.setUserEmails = (options, successC, errorC) => { * @param successC success callback function. */ appsFlyer.setAdditionalData = (additionalData, successC) => { - if (successC) { - return RNAppsFlyer.setAdditionalData(additionalData, successC); - } else { - return RNAppsFlyer.setAdditionalData(additionalData, (result) => console.log(result)); - } + if (successC) { + return RNAppsFlyer.setAdditionalData(additionalData, successC); + } else { + return RNAppsFlyer.setAdditionalData(additionalData, (result) => + console.log(result) + ); + } }; /** @@ -135,7 +272,7 @@ appsFlyer.setAdditionalData = (additionalData, successC) => { * @callback callback function that returns (error,uid) */ appsFlyer.getAppsFlyerUID = (callback) => { - return RNAppsFlyer.getAppsFlyerUID(callback); + return RNAppsFlyer.getAppsFlyerUID(callback); }; /** @@ -145,17 +282,19 @@ appsFlyer.getAppsFlyerUID = (callback) => { * @param successC success callback function. */ appsFlyer.updateServerUninstallToken = (token, successC) => { - if (token == null) { - token = ''; - } - if (typeof token != 'string') { - token = token.toString(); - } - if (successC) { - return RNAppsFlyer.updateServerUninstallToken(token, successC); - } else { - return RNAppsFlyer.updateServerUninstallToken(token, (result) => console.log(result)); - } + if (token == null) { + token = ""; + } + if (typeof token != "string") { + token = token.toString(); + } + if (successC) { + return RNAppsFlyer.updateServerUninstallToken(token, successC); + } else { + return RNAppsFlyer.updateServerUninstallToken(token, (result) => + console.log(result) + ); + } }; /** @@ -166,17 +305,19 @@ appsFlyer.updateServerUninstallToken = (token, successC) => { * @param successC callback function. */ appsFlyer.setCustomerUserId = (userId, successC) => { - if (userId == null) { - userId = ''; - } - if (typeof userId != 'string') { - userId = userId.toString(); - } - if (successC) { - return RNAppsFlyer.setCustomerUserId(userId, successC); - } else { - return RNAppsFlyer.setCustomerUserId(userId, (result) => console.log(result)); - } + if (userId == null) { + userId = ""; + } + if (typeof userId != "string") { + userId = userId.toString(); + } + if (successC) { + return RNAppsFlyer.setCustomerUserId(userId, successC); + } else { + return RNAppsFlyer.setCustomerUserId(userId, (result) => + console.log(result) + ); + } }; /** @@ -188,11 +329,11 @@ appsFlyer.setCustomerUserId = (userId, successC) => { * @param successC callback function. */ appsFlyer.stop = (isStopped, successC) => { - if (successC) { - return RNAppsFlyer.stop(isStopped, successC); - } else { - return RNAppsFlyer.stop(isStopped, (result) => console.log(result)); - } + if (successC) { + return RNAppsFlyer.stop(isStopped, successC); + } else { + return RNAppsFlyer.stop(isStopped, (result) => console.log(result)); + } }; /** @@ -205,7 +346,7 @@ appsFlyer.stop = (isStopped, successC) => { * @platform android */ appsFlyer.setCollectIMEI = (isCollect, successC) => { - return RNAppsFlyer.setCollectIMEI(isCollect, successC); + return RNAppsFlyer.setCollectIMEI(isCollect, successC); }; /** @@ -218,7 +359,7 @@ appsFlyer.setCollectIMEI = (isCollect, successC) => { * @platform android */ appsFlyer.setCollectAndroidID = (isCollect, successC) => { - return RNAppsFlyer.setCollectAndroidID(isCollect, successC); + return RNAppsFlyer.setCollectAndroidID(isCollect, successC); }; /** @@ -229,17 +370,19 @@ appsFlyer.setCollectAndroidID = (isCollect, successC) => { * @param successC callback function. */ appsFlyer.setAppInviteOneLinkID = (oneLinkID, successC) => { - if (oneLinkID == null) { - oneLinkID = ''; - } - if (typeof oneLinkID != 'string') { - oneLinkID = oneLinkID.toString(); - } - if (successC) { - return RNAppsFlyer.setAppInviteOneLinkID(oneLinkID, successC); - } else { - return RNAppsFlyer.setAppInviteOneLinkID(oneLinkID, (result) => console.log(result)); - } + if (oneLinkID == null) { + oneLinkID = ""; + } + if (typeof oneLinkID != "string") { + oneLinkID = oneLinkID.toString(); + } + if (successC) { + return RNAppsFlyer.setAppInviteOneLinkID(oneLinkID, successC); + } else { + return RNAppsFlyer.setAppInviteOneLinkID(oneLinkID, (result) => + console.log(result) + ); + } }; /** @@ -251,7 +394,7 @@ appsFlyer.setAppInviteOneLinkID = (oneLinkID, successC) => { * @param error error callback function. */ appsFlyer.generateInviteLink = (parameters, success, error) => { - return RNAppsFlyer.generateInviteLink(parameters, success, error); + return RNAppsFlyer.generateInviteLink(parameters, success, error); }; /** @@ -263,18 +406,18 @@ appsFlyer.generateInviteLink = (parameters, success, error) => { * @param parameters additional params to be added to the attribution link */ appsFlyer.logCrossPromotionImpression = (appId, campaign, parameters) => { - if (appId == null || appId == '') { - console.log('appid is missing!'); - return; - } - if (campaign == null) { - campaign = ''; - } - if (typeof appId != 'string' || typeof campaign != 'string') { - appId = appId.toString(); - campaign = campaign.toString(); - } - return RNAppsFlyer.logCrossPromotionImpression(appId, campaign, parameters); + if (appId == null || appId == "") { + console.log("appid is missing!"); + return; + } + if (campaign == null) { + campaign = ""; + } + if (typeof appId != "string" || typeof campaign != "string") { + appId = appId.toString(); + campaign = campaign.toString(); + } + return RNAppsFlyer.logCrossPromotionImpression(appId, campaign, parameters); }; /** @@ -285,18 +428,18 @@ appsFlyer.logCrossPromotionImpression = (appId, campaign, parameters) => { * @param params additional user params. */ appsFlyer.logCrossPromotionAndOpenStore = (appId, campaign, params) => { - if (appId == null || appId == '') { - console.log('appid is missing!'); - return; - } - if (campaign == null) { - campaign = ''; - } - if (typeof appId != 'string' || typeof campaign != 'string') { - appId = appId.toString(); - campaign = campaign.toString(); - } - return RNAppsFlyer.logCrossPromotionAndOpenStore(appId, campaign, params); + if (appId == null || appId == "") { + console.log("appid is missing!"); + return; + } + if (campaign == null) { + campaign = ""; + } + if (typeof appId != "string" || typeof campaign != "string") { + appId = appId.toString(); + campaign = campaign.toString(); + } + return RNAppsFlyer.logCrossPromotionAndOpenStore(appId, campaign, params); }; /** @@ -307,18 +450,20 @@ appsFlyer.logCrossPromotionAndOpenStore = (appId, campaign, params) => { * @param successC success callback function. */ appsFlyer.setCurrencyCode = (currencyCode, successC) => { - if (currencyCode == null || currencyCode == '') { - console.log('currencyCode is missing!'); - return; - } - if (typeof currencyCode != 'string') { - currencyCode = currencyCode.toString(); - } - if (successC) { - return RNAppsFlyer.setCurrencyCode(currencyCode, successC); - } else { - return RNAppsFlyer.setCurrencyCode(currencyCode, (result) => console.log(result)); - } + if (currencyCode == null || currencyCode == "") { + console.log("currencyCode is missing!"); + return; + } + if (typeof currencyCode != "string") { + currencyCode = currencyCode.toString(); + } + if (successC) { + return RNAppsFlyer.setCurrencyCode(currencyCode, successC); + } else { + return RNAppsFlyer.setCurrencyCode(currencyCode, (result) => + console.log(result) + ); + } }; /** @@ -336,107 +481,122 @@ appsFlyer.setCurrencyCode = (currencyCode, successC) => { * @returns {remove: function - unregister listener} */ appsFlyer.onInstallConversionData = (callback) => { - const listener = appsFlyerEventEmitter.addListener('onInstallConversionDataLoaded', (_data) => { - if (callback && typeof callback === typeof Function) { - try { - let data = JSON.parse(_data); - callback(data); - } catch (_error) { - //throw new AFParseJSONException("..."); - //TODO: for today we return an error in callback - callback(new AFParseJSONException('Invalid data structure', _data)); - } - } - }); - - eventsMap['onInstallConversionData'] = listener; - - // unregister listener (suppose should be called from componentWillUnmount() ) - return function remove() { - listener.remove(); - }; + const listener = appsFlyerEventEmitter.addListener( + "onInstallConversionDataLoaded", + (_data) => { + if (callback && typeof callback === typeof Function) { + try { + let data = JSON.parse(_data); + callback(data); + } catch (_error) { + //throw new AFParseJSONException("..."); + //TODO: for today we return an error in callback + callback(new AFParseJSONException("Invalid data structure", _data)); + } + } + } + ); + + eventsMap["onInstallConversionData"] = listener; + + // unregister listener (suppose should be called from componentWillUnmount() ) + return function remove() { + listener.remove(); + }; }; appsFlyer.onInstallConversionFailure = (callback) => { - const listener = appsFlyerEventEmitter.addListener('onInstallConversionFailure', (_data) => { - if (callback && typeof callback === typeof Function) { - try { - let data = JSON.parse(_data); - callback(data); - } catch (_error) { - //throw new AFParseJSONException("..."); - //TODO: for today we return an error in callback - callback(new AFParseJSONException('Invalid data structure', _data)); - } - } - }); - - eventsMap['onInstallConversionFailure'] = listener; - - // unregister listener (suppose should be called from componentWillUnmount() ) - return function remove() { - listener.remove(); - }; + const listener = appsFlyerEventEmitter.addListener( + "onInstallConversionFailure", + (_data) => { + if (callback && typeof callback === typeof Function) { + try { + let data = JSON.parse(_data); + callback(data); + } catch (_error) { + //throw new AFParseJSONException("..."); + //TODO: for today we return an error in callback + callback(new AFParseJSONException("Invalid data structure", _data)); + } + } + } + ); + + eventsMap["onInstallConversionFailure"] = listener; + + // unregister listener (suppose should be called from componentWillUnmount() ) + return function remove() { + listener.remove(); + }; }; appsFlyer.onAppOpenAttribution = (callback) => { - const listener = appsFlyerEventEmitter.addListener('onAppOpenAttribution', (_data) => { - if (callback && typeof callback === typeof Function) { - try { - let data = JSON.parse(_data); - callback(data); - } catch (_error) { - callback(new AFParseJSONException('Invalid data structure', _data)); - } - } - }); + const listener = appsFlyerEventEmitter.addListener( + "onAppOpenAttribution", + (_data) => { + if (callback && typeof callback === typeof Function) { + try { + let data = JSON.parse(_data); + callback(data); + } catch (_error) { + callback(new AFParseJSONException("Invalid data structure", _data)); + } + } + } + ); - eventsMap['onAppOpenAttribution'] = listener; + eventsMap["onAppOpenAttribution"] = listener; - // unregister listener (suppose should be called from componentWillUnmount() ) - return function remove() { - listener.remove(); - }; + // unregister listener (suppose should be called from componentWillUnmount() ) + return function remove() { + listener.remove(); + }; }; appsFlyer.onAttributionFailure = (callback) => { - const listener = appsFlyerEventEmitter.addListener('onAttributionFailure', (_data) => { - if (callback && typeof callback === typeof Function) { - try { - let data = JSON.parse(_data); - callback(data); - } catch (_error) { - callback(new AFParseJSONException('Invalid data structure', _data)); - } - } - }); + const listener = appsFlyerEventEmitter.addListener( + "onAttributionFailure", + (_data) => { + if (callback && typeof callback === typeof Function) { + try { + let data = JSON.parse(_data); + callback(data); + } catch (_error) { + callback(new AFParseJSONException("Invalid data structure", _data)); + } + } + } + ); - eventsMap['onAttributionFailure'] = listener; + eventsMap["onAttributionFailure"] = listener; - // unregister listener (suppose should be called from componentWillUnmount() ) - return function remove() { - listener.remove(); - }; + // unregister listener (suppose should be called from componentWillUnmount() ) + return function remove() { + listener.remove(); + }; }; appsFlyer.onDeepLink = (callback) => { - const listener = appsFlyerEventEmitter.addListener('onDeepLinking', (_data) => { - if (callback && typeof callback === typeof Function) { - try { - let data = JSON.parse(_data); - callback(data); - } catch (_error) { - callback(new AFParseJSONException('Invalid data structure', _data)); - } - } - }); + const listener = appsFlyerEventEmitter.addListener( + "onDeepLinking", + (_data) => { + if (callback && typeof callback === typeof Function) { + try { + let data = JSON.parse(_data); + callback(data); + } catch (_error) { + callback(new AFParseJSONException("Invalid data structure", _data)); + } + } + } + ); - eventsMap['onDeepLinking'] = listener; + eventsMap["onDeepLinking"] = listener; - // unregister listener (suppose should be called from componentWillUnmount() ) - return function remove() { - listener.remove(); - }; + // unregister listener (suppose should be called from componentWillUnmount() ) + return function remove() { + listener.remove(); + }; }; /** @@ -447,11 +607,13 @@ appsFlyer.onDeepLink = (callback) => { * @param successC success callback function. */ appsFlyer.anonymizeUser = (shouldAnonymize, successC) => { - if (successC) { - return RNAppsFlyer.anonymizeUser(shouldAnonymize, successC); - } else { - return RNAppsFlyer.anonymizeUser(shouldAnonymize, (result) => console.log(result)); - } + if (successC) { + return RNAppsFlyer.anonymizeUser(shouldAnonymize, successC); + } else { + return RNAppsFlyer.anonymizeUser(shouldAnonymize, (result) => + console.log(result) + ); + } }; /** @@ -463,7 +625,7 @@ appsFlyer.anonymizeUser = (shouldAnonymize, successC) => { * @param errorC error callback function. */ appsFlyer.setOneLinkCustomDomains = (domains, successC, errorC) => { - return RNAppsFlyer.setOneLinkCustomDomains(domains, successC, errorC); + return RNAppsFlyer.setOneLinkCustomDomains(domains, successC, errorC); }; /** @@ -476,7 +638,7 @@ appsFlyer.setOneLinkCustomDomains = (domains, successC, errorC) => { * @param errorC error callback function. */ appsFlyer.setResolveDeepLinkURLs = (urls, successC, errorC) => { - return RNAppsFlyer.setResolveDeepLinkURLs(urls, successC, errorC); + return RNAppsFlyer.setResolveDeepLinkURLs(urls, successC, errorC); }; /** @@ -489,10 +651,10 @@ appsFlyer.setResolveDeepLinkURLs = (urls, successC, errorC) => { * @param callback Result callback */ appsFlyer.performOnAppAttribution = (urlString, successC, errorC) => { - if (typeof urlString != 'string') { - urlString = urlString.toString(); - } - return RNAppsFlyer.performOnAppAttribution(urlString, successC, errorC); + if (typeof urlString != "string") { + urlString = urlString.toString(); + } + return RNAppsFlyer.performOnAppAttribution(urlString, successC, errorC); }; /** @@ -502,7 +664,7 @@ appsFlyer.performOnAppAttribution = (urlString, successC, errorC) => { */ appsFlyer.setSharingFilterForAllPartners = () => { - return appsFlyer.setSharingFilterForPartners(['all']); + return appsFlyer.setSharingFilterForPartners(["all"]); }; /** @@ -515,7 +677,7 @@ appsFlyer.setSharingFilterForAllPartners = () => { */ appsFlyer.setSharingFilter = (partners, successC, errorC) => { - return appsFlyer.setSharingFilterForPartners(partners); + return appsFlyer.setSharingFilterForPartners(partners); }; /** @@ -523,7 +685,7 @@ appsFlyer.setSharingFilter = (partners, successC, errorC) => { * @param shouldDisable Flag to disable/enable IDFA collection */ appsFlyer.disableAdvertisingIdentifier = (isDisable) => { - return RNAppsFlyer.disableAdvertisingIdentifier(isDisable); + return RNAppsFlyer.disableAdvertisingIdentifier(isDisable); }; /** @@ -532,7 +694,7 @@ appsFlyer.disableAdvertisingIdentifier = (isDisable) => { * @platform iOS only */ appsFlyer.disableIDFVCollection = (shouldDisable) => { - return RNAppsFlyer.disableIDFVCollection(shouldDisable); + return RNAppsFlyer.disableIDFVCollection(shouldDisable); }; /** @@ -541,7 +703,7 @@ appsFlyer.disableIDFVCollection = (shouldDisable) => { * @platform iOS only */ appsFlyer.disableCollectASA = (shouldDisable) => { - return RNAppsFlyer.disableCollectASA(shouldDisable); + return RNAppsFlyer.disableCollectASA(shouldDisable); }; /** @@ -552,11 +714,15 @@ appsFlyer.disableCollectASA = (shouldDisable) => { * @param errorC Error callback */ appsFlyer.validateAndLogInAppPurchase = (purchaseInfo, successC, errorC) => { - return RNAppsFlyer.validateAndLogInAppPurchase(purchaseInfo, successC, errorC); + return RNAppsFlyer.validateAndLogInAppPurchase( + purchaseInfo, + successC, + errorC + ); }; appsFlyer.setUseReceiptValidationSandbox = (isSandbox) => { - return RNAppsFlyer.setUseReceiptValidationSandbox(isSandbox); + return RNAppsFlyer.setUseReceiptValidationSandbox(isSandbox); }; /** @@ -567,7 +733,7 @@ appsFlyer.setUseReceiptValidationSandbox = (isSandbox) => { * @param pushPayload */ appsFlyer.sendPushNotificationData = (pushPayload, errorC = null) => { - return RNAppsFlyer.sendPushNotificationData(pushPayload, errorC); + return RNAppsFlyer.sendPushNotificationData(pushPayload, errorC); }; /** @@ -577,7 +743,7 @@ appsFlyer.sendPushNotificationData = (pushPayload, errorC = null) => { * @param successC: success callback */ appsFlyer.setHost = (hostPrefix, hostName, successC) => { - RNAppsFlyer.setHost(hostPrefix, hostName, successC); + RNAppsFlyer.setHost(hostPrefix, hostName, successC); }; /** @@ -588,7 +754,7 @@ appsFlyer.setHost = (hostPrefix, hostName, successC) => { * @param errorC: error callback */ appsFlyer.addPushNotificationDeepLinkPath = (path, successC, errorC) => { - RNAppsFlyer.addPushNotificationDeepLinkPath(path, successC, errorC); + RNAppsFlyer.addPushNotificationDeepLinkPath(path, successC, errorC); }; /** @@ -596,7 +762,7 @@ appsFlyer.addPushNotificationDeepLinkPath = (path, successC, errorC) => { * @param isDisabled */ appsFlyer.disableSKAD = (disableSkad) => { - return RNAppsFlyer.disableSKAD(disableSkad); + return RNAppsFlyer.disableSKAD(disableSkad); }; /** @@ -604,16 +770,16 @@ appsFlyer.disableSKAD = (disableSkad) => { * @param language */ appsFlyer.setCurrentDeviceLanguage = (language) => { - if (typeof language === 'string') { - return RNAppsFlyer.setCurrentDeviceLanguage(language); - } + if (typeof language === "string") { + return RNAppsFlyer.setCurrentDeviceLanguage(language); + } }; /** * Used by advertisers to exclude specified networks/integrated partners from getting data. */ appsFlyer.setSharingFilterForPartners = (partners) => { - return RNAppsFlyer.setSharingFilterForPartners(partners); + return RNAppsFlyer.setSharingFilterForPartners(partners); }; /** * Allows sending custom data for partner integration purposes. @@ -621,9 +787,9 @@ appsFlyer.setSharingFilterForPartners = (partners) => { * @param partnerData: Customer data, depends on the integration configuration with the specific partner. */ appsFlyer.setPartnerData = (partnerId, partnerData) => { - if (typeof partnerId === 'string' && typeof partnerData === 'object') { - return RNAppsFlyer.setPartnerData(partnerId, partnerData); - } + if (typeof partnerId === "string" && typeof partnerData === "object") { + return RNAppsFlyer.setPartnerData(partnerId, partnerData); + } }; /** @@ -632,65 +798,76 @@ appsFlyer.setPartnerData = (partnerId, partnerData) => { * @param parameters: Parameters to append to the deeplink url after it passed validation. */ appsFlyer.appendParametersToDeepLinkingURL = (contains, parameters) => { - if (typeof contains === 'string' && typeof parameters === 'object') { - return RNAppsFlyer.appendParametersToDeepLinkingURL(contains, parameters); - } + if (typeof contains === "string" && typeof parameters === "object") { + return RNAppsFlyer.appendParametersToDeepLinkingURL(contains, parameters); + } }; appsFlyer.setDisableNetworkData = (disable) => { - return RNAppsFlyer.setDisableNetworkData(disable); + return RNAppsFlyer.setDisableNetworkData(disable); }; appsFlyer.startSdk = () => { - return RNAppsFlyer.startSdk(); + return RNAppsFlyer.startSdk(); }; appsFlyer.performOnDeepLinking = () => { - return RNAppsFlyer.performOnDeepLinking(); + return RNAppsFlyer.performOnDeepLinking(); }; /** * instruct the SDK to collect the TCF data from the device. - * @param enabled: if the sdk should collect the TCF data. true/false + * @param enabled: if the sdk should collect the TCF data. true/false */ -appsFlyer.enableTCFDataCollection= (enabled) => { - return RNAppsFlyer.enableTCFDataCollection(enabled); -} +appsFlyer.enableTCFDataCollection = (enabled) => { + return RNAppsFlyer.enableTCFDataCollection(enabled); +}; /** * If your app does not use a CMP compatible with TCF v2.2, use the SDK API detailed below to provide the consent data directly to the SDK. * @param consentData: AppsFlyerConsent object. */ appsFlyer.setConsentData = (consentData) => { - return RNAppsFlyer.setConsentData(consentData); -} + return RNAppsFlyer.setConsentData(consentData); +}; function AFParseJSONException(_message, _data) { - this.message = _message; - this.data = _data; - this.name = 'AFParseJSONException'; + this.message = _message; + this.data = _data; + this.name = "AFParseJSONException"; } // Consent object export const AppsFlyerConsent = (function () { - // Private constructor - function AppsFlyerConsent(isUserSubjectToGDPR, hasConsentForDataUsage, hasConsentForAdsPersonalization) { - this.isUserSubjectToGDPR = isUserSubjectToGDPR; - this.hasConsentForDataUsage = hasConsentForDataUsage; - this.hasConsentForAdsPersonalization = hasConsentForAdsPersonalization; - } - - return { - // Factory method for GDPR user - forGDPRUser: function(hasConsentForDataUsage, hasConsentForAdsPersonalization) { - return new AppsFlyerConsent(true, hasConsentForDataUsage, hasConsentForAdsPersonalization); - }, - - // Factory method for non GDPR user - forNonGDPRUser: function() { - return new AppsFlyerConsent(false, null, null); - } - }; + // Private constructor + function AppsFlyerConsent( + isUserSubjectToGDPR, + hasConsentForDataUsage, + hasConsentForAdsPersonalization + ) { + this.isUserSubjectToGDPR = isUserSubjectToGDPR; + this.hasConsentForDataUsage = hasConsentForDataUsage; + this.hasConsentForAdsPersonalization = hasConsentForAdsPersonalization; + } + + return { + // Factory method for GDPR user + forGDPRUser: function ( + hasConsentForDataUsage, + hasConsentForAdsPersonalization + ) { + return new AppsFlyerConsent( + true, + hasConsentForDataUsage, + hasConsentForAdsPersonalization + ); + }, + + // Factory method for non GDPR user + forNonGDPRUser: function () { + return new AppsFlyerConsent(false, null, null); + }, + }; })(); -export default appsFlyer; \ No newline at end of file +export default appsFlyer; diff --git a/ios/PCAppsFlyer.h b/ios/PCAppsFlyer.h new file mode 100644 index 00000000..cfa7e776 --- /dev/null +++ b/ios/PCAppsFlyer.h @@ -0,0 +1,16 @@ +#if __has_include() // ver >= 0.40 +#import +#import +#else // ver < 0.40 +#import "RCTBridgeModule.h" +#import "RCTEventEmitter.h" +#endif + +#import +#import + +@interface PCAppsFlyer: RCTEventEmitter +// Define any properties and methods the PCAppsFlyer module will need +@end + +// Define any static constants related to PCAppsFlyer. diff --git a/ios/PCAppsFlyer.m b/ios/PCAppsFlyer.m new file mode 100644 index 00000000..244da017 --- /dev/null +++ b/ios/PCAppsFlyer.m @@ -0,0 +1,99 @@ +#import "PCAppsFlyer.h" +#import "RNAppsFlyer.h" +#import + +#import +#import + +@implementation PCAppsFlyer +@synthesize bridge = _bridge; + +static NSString *const TAG = @"[AppsFlyer_PurchaseConnector] "; +static NSString *const logSubscriptionsKey = @"logSubscriptions"; +static NSString *const logInAppsKey = @"logInApps"; +static NSString *const sandboxKey = @"sandbox"; + +PurchaseConnector *connector; + +// This RCT_EXPORT_MODULE macro exports the module to React Native. +RCT_EXPORT_MODULE(); + +RCT_EXPORT_METHOD(create:(NSDictionary *)config + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + NSLog(@"%@Attempting to configure PurchaseConnector.", TAG); + + // Perform a check to ensure that we do not reconfigure an existing connector. + if (connector != nil) { + reject(@"401", @"Connector already configured", nil); + return; + } + + // Obtain a shared instance of PurchaseConnector + connector = [PurchaseConnector shared]; + [connector setPurchaseRevenueDelegate: PCAppsFlyer.self]; + [connector setPurchaseRevenueDataSource:PCAppsFlyer.self]; + + BOOL logSubscriptions = [config[logSubscriptionsKey] boolValue]; + BOOL logInApps = [config[logInAppsKey] boolValue]; + BOOL sandbox = [config[sandboxKey] boolValue]; + + [connector setIsSandbox:sandbox]; + + // Based on the arguments, insert the corresponding options. + if (logSubscriptions) { + [connector setAutoLogPurchaseRevenue:AFSDKAutoLogPurchaseRevenueOptionsAutoRenewableSubscriptions]; + } + if (logInApps) { + [connector setAutoLogPurchaseRevenue:AFSDKAutoLogPurchaseRevenueOptionsInAppPurchases]; + } + + NSLog(@"%@Purchase Connector is configured successfully.", TAG); + resolve(nil); +} + +RCT_EXPORT_METHOD(startObservingTransactions:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + NSLog(@"%@Starting to observe transactions.", TAG); + if (connector == nil) { + reject(@"404", @"Connector not configured, did you call `create` first?", nil); + } else { + [connector startObservingTransactions]; + NSLog(@"%@Started observing transactions.", TAG); + resolve(nil); + } +} + +RCT_EXPORT_METHOD(stopObservingTransactions:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + NSLog(@"%@Stopping the observation of transactions.", TAG); + if (connector == nil) { + reject(@"404", @"Connector not configured, did you call `create` first?", nil); + } else { + [connector stopObservingTransactions]; + NSLog(@"%@Stopped observing transactions.", TAG); + resolve(nil); + } +} + +- (void)didReceivePurchaseRevenueValidationInfo:(nullable NSDictionary *)validationInfo error:(nullable NSError *)error { + // Send the validation info and error back to React Native. + // Call this function from the main thread. + [self sendEventWithName:@"onDidReceivePurchaseRevenueValidationInfo" body:@{@"validationInfo": validationInfo ?: [NSNull null], @"error": [self errorAsDictionary:error] ?: [NSNull null]}]; +} + +- (NSDictionary *)errorAsDictionary:(NSError *)error { + if (!error) return nil; + return @{ + @"localizedDescription": [error localizedDescription], + @"domain": [error domain], + @"code": @([error code]) + }; +} + +// Required by RCTEventEmitter: +- (NSArray *)supportedEvents { + return @[@"onDidReceivePurchaseRevenueValidationInfo"]; +} + +@end diff --git a/react-native-appsflyer.podspec b/react-native-appsflyer.podspec index e97da63c..4e3c7b35 100644 --- a/react-native-appsflyer.podspec +++ b/react-native-appsflyer.podspec @@ -15,16 +15,22 @@ Pod::Spec.new do |s| s.static_framework = true s.dependency 'React' + # AppsFlyerPurchaseConnector + if defined?($AppsFlyerPurchaseConnector) && ($AppsFlyerPurchaseConnector == true) + Pod::UI.puts "#{s.name}: Including PurchaseConnector." + s.dependency 'PurchaseConnector', '6.15.2' + end + # AppsFlyerFramework if defined?($RNAppsFlyerStrictMode) && ($RNAppsFlyerStrictMode == true) Pod::UI.puts "#{s.name}: Using AppsFlyerFramework/Strict mode" - s.dependency 'AppsFlyerFramework/Strict', '6.15.1' + s.dependency 'AppsFlyerFramework/Strict', '6.15.2' s.xcconfig = {'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) AFSDK_NO_IDFA=1' } else if !defined?($RNAppsFlyerStrictMode) Pod::UI.puts "#{s.name}: Using default AppsFlyerFramework. You may require App Tracking Transparency. Not allowed for Kids apps." Pod::UI.puts "#{s.name}: You may set variable `$RNAppsFlyerStrictMode=true` in Podfile to use strict mode for kids apps." end - s.dependency 'AppsFlyerFramework', '6.15.1' + s.dependency 'AppsFlyerFramework', '6.15.2' end end diff --git a/react-native.config.js b/react-native.config.js index 9fe432f3..0f30744a 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -4,8 +4,14 @@ module.exports = { dependency: { platforms: { android: { - packageImportPath: 'import com.appsflyer.reactnative.RNAppsFlyerPackage;', - packageInstance: 'new RNAppsFlyerPackage()', + packageImportPath: [ + 'import com.appsflyer.reactnative.RNAppsFlyerPackage;', + 'import com.appsflyer.reactnative.PCAppsFlyerPackage;', + ].join('\n'), + packageInstance: [ + 'new RNAppsFlyerPackage()', + 'new PCAppsFlyerPackage()', + ].join(',\n'), }, }, }, From 2a293ee65ee1a6702eaf83a6c163e1edd90b58f7 Mon Sep 17 00:00:00 2001 From: Amit Levy Date: Sun, 6 Oct 2024 11:27:46 +0300 Subject: [PATCH 3/9] Separated callbacks to onSuccess onFailure, modified native impl accordingly --- PurchaseConnector/constants/constants.ts | 8 +- .../utils/connector_callbacks.ts | 2 +- .../reactnative/PCAppsFlyerModule.java | 8 +- .../reactnative/RNAppsFlyerConstants.java | 6 +- demos/appsflyer-react-native-app/.gitignore | 3 + demos/appsflyer-react-native-app/App.js | 3 + .../android/app/build.gradle | 2 + .../android/build.gradle | 2 + .../babel.config.js | 17 ++- .../components/AppsFlyer.js | 4 +- .../components/Cart.js | 130 +++++++++++----- .../project.pbxproj | 12 +- demos/appsflyer-react-native-app/package.json | 7 +- index.d.ts | 6 +- index.js | 140 ++++++++++++------ ios/PCAppsFlyer.m | 9 +- 16 files changed, 245 insertions(+), 114 deletions(-) diff --git a/PurchaseConnector/constants/constants.ts b/PurchaseConnector/constants/constants.ts index 78aacf6a..0e73fe2d 100644 --- a/PurchaseConnector/constants/constants.ts +++ b/PurchaseConnector/constants/constants.ts @@ -3,10 +3,10 @@ class AppsFlyerConstants { static readonly MISSING_CONFIGURATION_EXCEPTION_MSG: string = "Could not create an instance without configuration"; // Adding method constants - static readonly SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER: string = - "onSubscriptionValidationResult"; - static readonly IN_APP_VALIDATION_RESULT_LISTENER: string = - "onInAppPurchaseValidationResult"; + static readonly SUBSCRIPTION_VALIDATION_SUCCESS: string = 'subscriptionValidationSuccess'; + static readonly SUBSCRIPTION_VALIDATION_FAILURE: string = 'subscriptionValidationFailure'; + static readonly IN_APP_PURCHASE_VALIDATION_SUCCESS: string = 'inAppPurchaseValidationSuccess'; + static readonly IN_APP_PURCHASE_VALIDATION_FAILURE: string = 'inAppPurchaseValidationFailure'; static readonly DID_RECEIVE_PURCHASE_REVENUE_VALIDATION_INFO: string = "onDidReceivePurchaseRevenueValidationInfo"; diff --git a/PurchaseConnector/utils/connector_callbacks.ts b/PurchaseConnector/utils/connector_callbacks.ts index 903836b7..decbbb8e 100644 --- a/PurchaseConnector/utils/connector_callbacks.ts +++ b/PurchaseConnector/utils/connector_callbacks.ts @@ -4,7 +4,7 @@ import { IosError, JVMThrowable } from "../models"; export type PurchaseConnectorListener = (data: any) => void; // Type definition for a listener which gets called when the `PurchaseConnectorImpl` receives purchase revenue validation info for iOS. -export type DidReceivePurchaseRevenueValidationInfo = (validationInfo?: Map, error?: IosError) => void; +export type onReceivePurchaseRevenueValidationInfo = (validationInfo?: Map, error?: IosError) => void; // Invoked when a 200 OK response is received from the server. // Note: An INVALID purchase is considered to be a successful response and will also be returned by this callback. diff --git a/android/src/main/java/com/appsflyer/reactnative/PCAppsFlyerModule.java b/android/src/main/java/com/appsflyer/reactnative/PCAppsFlyerModule.java index 3d01a8f6..9c95b9a4 100644 --- a/android/src/main/java/com/appsflyer/reactnative/PCAppsFlyerModule.java +++ b/android/src/main/java/com/appsflyer/reactnative/PCAppsFlyerModule.java @@ -131,12 +131,12 @@ public void stopObservingTransactions() { private final MappedValidationResultListener arsListener = new MappedValidationResultListener() { @Override public void onFailure(String result, Throwable error) { - handleError(EVENT_SUBSCRIPTION_VALIDATION_RESULT, result, error); + handleError(EVENT_SUBSCRIPTION_VALIDATION_FAILURE, result, error); } @Override public void onResponse(Map response) { - handleSuccess(EVENT_SUBSCRIPTION_VALIDATION_RESULT, response); + handleSuccess(EVENT_SUBSCRIPTION_VALIDATION_SUCCESS, response); } }; @@ -144,12 +144,12 @@ public void onResponse(Map response) { private final MappedValidationResultListener viapListener = new MappedValidationResultListener() { @Override public void onFailure(String result, Throwable error) { - handleError(EVENT_IN_APP_PURCHASE_VALIDATION_RESULT, result, error); + handleError(EVENT_IN_APP_PURCHASE_VALIDATION_FAILURE, result, error); } @Override public void onResponse(Map response) { - handleSuccess(EVENT_IN_APP_PURCHASE_VALIDATION_RESULT, response); + handleSuccess(EVENT_IN_APP_PURCHASE_VALIDATION_SUCCESS, response); } }; diff --git a/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java b/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java index 21aff74c..c14eeb51 100755 --- a/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java +++ b/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java @@ -59,8 +59,10 @@ public class RNAppsFlyerConstants { final static String AF_ADDITIONAL_PARAMETERS = "additionalParameters"; //Purchase Connector - final static String EVENT_SUBSCRIPTION_VALIDATION_RESULT = "onSubscriptionValidationResult"; - final static String EVENT_IN_APP_PURCHASE_VALIDATION_RESULT = "onInAppPurchaseValidationResult"; + final static String EVENT_SUBSCRIPTION_VALIDATION_SUCCESS = "subscriptionValidationSuccess"; + final static String EVENT_SUBSCRIPTION_VALIDATION_FAILURE = "subscriptionValidationFailure"; + final static String EVENT_IN_APP_PURCHASE_VALIDATION_SUCCESS = "inAppPurchaseValidationSuccess"; + final static String EVENT_IN_APP_PURCHASE_VALIDATION_FAILURE = "inAppPurchaseValidationFailure"; final static String ENABLE_MODULE_MESSAGE = "Please set appsflyer.enable_purchase_connector to true in your gradle.properties file."; } diff --git a/demos/appsflyer-react-native-app/.gitignore b/demos/appsflyer-react-native-app/.gitignore index ad572e63..8631d5c4 100644 --- a/demos/appsflyer-react-native-app/.gitignore +++ b/demos/appsflyer-react-native-app/.gitignore @@ -57,3 +57,6 @@ buck-out/ # CocoaPods /ios/Pods/ + +#Secrets +.env diff --git a/demos/appsflyer-react-native-app/App.js b/demos/appsflyer-react-native-app/App.js index 18aded76..789c353e 100644 --- a/demos/appsflyer-react-native-app/App.js +++ b/demos/appsflyer-react-native-app/App.js @@ -25,6 +25,7 @@ import {createStackNavigator} from '@react-navigation/stack'; import HomeScreen from './components/HomeScreen.js'; import Cart from './components/Cart.js'; import Item from './components/Item.js'; +import {withIAPContext} from 'react-native-iap'; const Stack = createStackNavigator(); try { @@ -59,3 +60,5 @@ const App = () => { const styles = StyleSheet.create({}); export default App; + +//export default withIAPContext(App); diff --git a/demos/appsflyer-react-native-app/android/app/build.gradle b/demos/appsflyer-react-native-app/android/app/build.gradle index 7ce0d22f..e6b0cbbb 100644 --- a/demos/appsflyer-react-native-app/android/app/build.gradle +++ b/demos/appsflyer-react-native-app/android/app/build.gradle @@ -131,6 +131,8 @@ android { targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0" + //RNIap + missingDimensionStrategy "store", "play" } splits { abi { diff --git a/demos/appsflyer-react-native-app/android/build.gradle b/demos/appsflyer-react-native-app/android/build.gradle index 002b28af..6613c0dc 100644 --- a/demos/appsflyer-react-native-app/android/build.gradle +++ b/demos/appsflyer-react-native-app/android/build.gradle @@ -7,6 +7,8 @@ buildscript { compileSdkVersion = 34 targetSdkVersion = 34 ndkVersion = "20.1.5948944" + //For RNIap + supportLibVersion = "28.0.0" } repositories { google() diff --git a/demos/appsflyer-react-native-app/babel.config.js b/demos/appsflyer-react-native-app/babel.config.js index f842b77f..7fbf1d05 100644 --- a/demos/appsflyer-react-native-app/babel.config.js +++ b/demos/appsflyer-react-native-app/babel.config.js @@ -1,3 +1,16 @@ module.exports = { - presets: ['module:metro-react-native-babel-preset'], -}; + presets: ["module:metro-react-native-babel-preset"], + plugins: [ + [ + "module:react-native-dotenv", + { + moduleName: "@env", + path: ".env", + blacklist: null, + whitelist: null, + safe: false, + allowUndefined: true, + }, + ], + ], +}; \ No newline at end of file diff --git a/demos/appsflyer-react-native-app/components/AppsFlyer.js b/demos/appsflyer-react-native-app/components/AppsFlyer.js index a6b992e6..71184660 100644 --- a/demos/appsflyer-react-native-app/components/AppsFlyer.js +++ b/demos/appsflyer-react-native-app/components/AppsFlyer.js @@ -14,11 +14,11 @@ export const AF_clickOnItem = 'af_click_on_item'; const initOptions = { isDebug: true, - devKey: 'Us4GmXxXx46Qed', + devKey: 'WdpTVAcYwmxsaQ4WeTspmh', onInstallConversionDataListener: true, timeToWaitForATTUserAuthorization: 10, onDeepLinkListener: true, - appId: '741993747', + appId: '1201211633', }; // AppsFlyer initialization flow. ends with initSdk. diff --git a/demos/appsflyer-react-native-app/components/Cart.js b/demos/appsflyer-react-native-app/components/Cart.js index 49572da8..a4336451 100644 --- a/demos/appsflyer-react-native-app/components/Cart.js +++ b/demos/appsflyer-react-native-app/components/Cart.js @@ -1,53 +1,105 @@ /* @flow weak */ import React from 'react'; -import {View, Text, StyleSheet, ScrollView} from 'react-native'; -import {ListItem, Avatar, Button} from 'react-native-elements' +import {View, Text, StyleSheet, ScrollView, Alert} from 'react-native'; +import {ListItem, Avatar, Button} from 'react-native-elements'; +//import {InAppPurchase} from 'react-native-iap'; const Cart = ({route, navigation}) => { - const {productList, removeProductFromCart, checkout} = route.params; + const {productList, removeProductFromCart, checkout} = route.params; + + /* + const productIds = [ + 'one1', + 'non.cons2', + 'auto.renew', + 'non.renew', + 'cons.test', + 'nonconsumable.purchase1', + 'autorenewable.purchase1', + 'nonrenewing.purchase1', + ]; - const handleRemove = product => { - removeProductFromCart(product); - navigation.goBack(); - } + InAppPurchase.getProducts(productIds) + .then(products => { + console.log('Products:', products); + }) + .catch(error => { + console.log('Error fetching products:', error); + }); + */ + + const handleRemove = product => { + removeProductFromCart(product); + navigation.goBack(); + }; - const handleCheckout = () => { - if (productList.length !== 0) { - checkout(); - navigation.goBack(); - } - } + const handleCheckout = () => { + if (productList.length !== 0) { + checkout(); + navigation.goBack(); + } else { + Alert.alert( + 'Cart Empty', // Alert Title + 'The cart is empty.', // Alert Message + [ + { + text: 'OK', + onPress: () => console.log('OK Pressed'), + style: 'cancel', + }, + ], + { + cancelable: true, // whether the alert can be dismissed by tapping outside of the alert box + onDismiss: () => console.log('Alert dismissed'), // a callback that gets called when the alert is dismissed + }, + ); + } + }; - return ( + return ( + - { - productList.map((product, index) => { - return ( handleRemove(product) - } - />}> - - - {product.name} - {`${product.price} USD`} - - ) - }) - } + {productList.map((product, index) => { + return ( + handleRemove(product)} + /> + }> + + + {product.name} + {`${product.price} USD`} + + + ); + })} -