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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Versions

## 6.17.8

- Updated Android SDK from 6.17.4 to 6.17.5
- Updated iOS SDK from 6.17.7 to 6.17.8
- Updated iOS Purchase Connector from 6.17.7 to 6.17.8
- Deprecated `validateAndLogInAppAndroidPurchase` (V1) - use `validateAndLogInAppPurchaseV2` instead
- Deprecated `validateAndLogInAppIosPurchase` (V1) - use `validateAndLogInAppPurchaseV2` instead
- Enhanced iOS error handling for `validateAndLogInAppPurchaseV2` with comprehensive NSError parsing (code, domain, userInfo)
- **Documentation Updates:**
- Removed "Beta" label from `validateAndLogInAppPurchaseV2` API
- Marked V1 purchase validation APIs as Deprecated
- Added comprehensive `PlatformException` error handling examples for V2 API
- Added iOS token format explanation for uninstall measurement
- Added cross-platform Firebase Messaging example for uninstall tokens

## 6.17.7+1

- Update Android SDK version to 6.17.4
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ To do so, please follow [this article](https://support.appsflyer.com/hc/en-us/ar

## SDK Versions

- Android AppsFlyer SDK **v6.17.3**
- iOS AppsFlyer SDK **v6.17.7**
- Android AppsFlyer SDK **v6.17.5**
- iOS AppsFlyer SDK **v6.17.8**

### Purchase Connector versions

- Android 2.2.0
- iOS 6.17.7
- iOS 6.17.8

## ❗❗ Breaking changes when updating to v6.x.x❗❗

Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.0.0'
implementation 'com.appsflyer:af-android-sdk:6.17.4'
implementation 'com.appsflyer:af-android-sdk:6.17.5'
implementation 'com.android.installreferrer:installreferrer:2.2'
// implementation 'androidx.core:core-ktx:1.13.1'
if (includeConnector) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.appsflyer.appsflyersdk;

public final class AppsFlyerConstants {
final static String PLUGIN_VERSION = "6.17.7+1";
final static String PLUGIN_VERSION = "6.17.8";
final static String AF_APP_INVITE_ONE_LINK = "appInviteOneLink";
final static String AF_HOST_PREFIX = "hostPrefix";
final static String AF_HOST_NAME = "hostName";
Expand Down
70 changes: 59 additions & 11 deletions doc/AdvancedAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,47 @@ You can register the uninstall token with AppsFlyer by calling the following API
appsFlyerSdk.updateServerUninstallToken("token");
```

> **Note:** When using this method on iOS, the token should be passed as a **hexadecimal string representation** of the device token. The plugin will automatically convert the hex string to the required `NSData` format for the AppsFlyer SDK.
>
> If you're using the [firebase_messaging](https://pub.dev/packages/firebase_messaging) plugin, you can get the APNs token on iOS using `FirebaseMessaging.instance.getAPNSToken()` which returns the token as a hex string, which is the expected format for this method.

### Android

It is possible to utilize the [Firebase Messaging Plugin for Flutter](https://pub.dev/packages/firebase_messaging) for everything related to the uninstall token.
You can read more about Android Uninstall Measurement in our [knowledge base](https://support.appsflyer.com/hc/en-us/articles/4408933557137) and you can follow our guide for Uninstall measurement using FCM on our [DevHub](https://dev.appsflyer.com/hc/docs/uninstall-measurement-android).

On the flutter side, you can register the uninstall token with AppsFlyer by calling the following API with your uninstall token:
On the Flutter side, you can register the uninstall token with AppsFlyer by calling the following API with your uninstall token:
```dart
appsFlyerSdk.updateServerUninstallToken("token");
```

**Example using Firebase Messaging (cross-platform):**
```dart
import 'dart:io' show Platform;
import 'package:firebase_messaging/firebase_messaging.dart';

// Update uninstall token for AppsFlyer
void _updateUninstallToken(appsFlyerSdk) {
if (Platform.isAndroid) {
FirebaseMessaging.instance.getToken().then((token) {
if (token != null) {
appsFlyerSdk.updateServerUninstallToken(token);
}
});
} else if (Platform.isIOS) {
FirebaseMessaging.instance.getAPNSToken().then((token) {
if (token != null) {
appsFlyerSdk.updateServerUninstallToken(token);
}
});
}
}
```
**Note:**
- On Android, `getToken()` returns the FCM token.
- On iOS, `getAPNSToken()` returns the APNs token as a hex string, suitable for `updateServerUninstallToken`.
- Replace `appsFlyerSdk` with your instance of `AppsflyerSdk`.

---

## <a id="user-invite"> User invite
Expand Down Expand Up @@ -114,11 +145,9 @@ appsFlyerSdk.generateInviteLink(inviteLinkParams,
Receipt validation is a secure mechanism whereby the payment platform (e.g. Apple or Google) validates that an in-app purchase indeed occurred as reported.<br>
Learn more - https://support.appsflyer.com/hc/en-us/articles/207032106-Receipt-validation-for-in-app-purchases<br>

**Cross-Platform V2 API (Recommended - SDK v6.17.3+) - BETA:**
**Cross-Platform V2 API (Recommended - SDK v6.17.3+):**

> ⚠️ **BETA Feature**: This API is currently in beta. While it's stable and recommended for new implementations, please test thoroughly in your environment before production use.

The new unified purchase validation API that works across both Android and iOS platforms:
The unified purchase validation API that works across both Android and iOS platforms:

```dart
Future<Map<String, dynamic>> validateAndLogInAppPurchaseV2(
Expand All @@ -135,7 +164,7 @@ AFPurchaseDetails(
)
```

Example:
**Example:**
```dart
// Create purchase details
AFPurchaseDetails purchaseDetails = AFPurchaseDetails(
Expand All @@ -151,24 +180,40 @@ try {
additionalParameters: {"custom_param": "value"}
);
print("Validation successful: $result");
} on PlatformException catch (e) {
// Handle platform-specific errors with detailed information
print("Validation failed: ${e.message}");
print("Error code: ${e.code}");
if (e.details != null) {
// Access detailed error information
final details = e.details as Map<String, dynamic>;
print("Error details: $details");
// On iOS, additional fields may include:
// - error_code: The NSError code
// - error_domain: The NSError domain
// - error_user_info: Additional error context
}
} catch (e) {
print("Validation failed: $e");
print("Unexpected error: $e");
}
```

**Benefits of V2 API:**
- ✅ **Cross-platform**: Single API works on both Android and iOS
- ✅ **Type-safe**: Uses structured data classes instead of raw strings
- ✅ **Better error handling**: Returns structured error information
- ✅ **Comprehensive error handling**: Returns structured error information including NSError details on iOS
- ✅ **Enhanced validation**: Uses AppsFlyer's latest validation infrastructure
- ✅ **Future-proof**: Built for AppsFlyer's V2 validation endpoints

---

**Legacy Platform-Specific APIs:**
**Deprecated Platform-Specific APIs:**

> ⚠️ **Deprecated**: The following platform-specific APIs are deprecated and will be removed in a future version. Please migrate to `validateAndLogInAppPurchaseV2` for cross-platform support.

**Android:**
**Android (Deprecated):**
```dart
@Deprecated('Use validateAndLogInAppPurchaseV2 instead')
Future<dynamic> validateAndLogInAppAndroidPurchase(
String publicKey,
String signature,
Expand All @@ -179,6 +224,7 @@ Future<dynamic> validateAndLogInAppAndroidPurchase(
```
Example:
```dart
// Deprecated - migrate to validateAndLogInAppPurchaseV2
appsFlyerSdk.validateAndLogInAppAndroidPurchase(
"publicKey",
"signature",
Expand All @@ -188,12 +234,13 @@ appsFlyerSdk.validateAndLogInAppAndroidPurchase(
{"fs": "fs"});
```

**iOS:**
**iOS (Deprecated):**

❗Important❗ for iOS - set SandBox to ```true```<br>
```appsFlyer.useReceiptValidationSandbox(true);```

```dart
@Deprecated('Use validateAndLogInAppPurchaseV2 instead')
Future<dynamic> validateAndLogInAppIosPurchase(
String productIdentifier,
String price,
Expand All @@ -204,6 +251,7 @@ Future<dynamic> validateAndLogInAppIosPurchase(

Example:
```dart
// Deprecated - migrate to validateAndLogInAppPurchaseV2
appsFlyerSdk.validateAndLogInAppIosPurchase(
"productIdentifier",
"price",
Expand Down
2 changes: 1 addition & 1 deletion example/ios/Flutter/AppFrameworkInfo.plist
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
<string>13.0</string>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
Expand Down Expand Up @@ -44,6 +44,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
Expand Down Expand Up @@ -72,6 +73,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
Expand Down
2 changes: 1 addition & 1 deletion ios/Classes/AppsflyerSdkPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
@end

// Appsflyer JS objects
#define kAppsFlyerPluginVersion @"6.17.6"
#define kAppsFlyerPluginVersion @"6.17.8"
#define afDevKey @"afDevKey"
#define afAppId @"afAppId"
#define afIsDebug @"isDebug"
Expand Down
117 changes: 43 additions & 74 deletions ios/Classes/AppsflyerSdkPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -638,87 +638,56 @@ - (void)validateAndLogInAppPurchase:(FlutterMethodCall*)call result:(FlutterResu
result(nil);
}

- (void)validateAndLogInAppPurchaseV2:(FlutterMethodCall*)call result:(FlutterResult)result{
@try {
// Extract purchase details map from Flutter
NSDictionary* purchaseDetailsMap = call.arguments[@"purchaseDetails"];
NSDictionary* additionalParameters = call.arguments[@"additionalParameters"];

if (purchaseDetailsMap == nil) {
result([FlutterError errorWithCode:@"INVALID_ARGUMENTS"
message:@"Purchase details cannot be null"
details:nil]);
return;
}

// Extract individual fields from purchase details map
NSString* purchaseTypeString = purchaseDetailsMap[@"purchaseType"];
NSString* purchaseToken = purchaseDetailsMap[@"purchaseToken"];
NSString* productId = purchaseDetailsMap[@"productId"];

// Validate required fields
if (purchaseTypeString == nil || purchaseToken == nil || productId == nil) {
result([FlutterError errorWithCode:@"INVALID_ARGUMENTS"
message:@"Purchase details must contain purchaseType, purchaseToken, and productId"
details:nil]);
return;
}

// Map Dart enum values to iOS purchase type
// For iOS, we use transactionId instead of purchaseToken, so we'll use purchaseToken as transactionId
NSString* transactionId = purchaseToken;

NSLog(@"AppsFlyer Debug: validateAndLogInAppPurchaseV2 called with purchaseType: %@, transactionId: %@, productId: %@", purchaseTypeString, transactionId, productId);

// Call the actual AppsFlyer iOS V2 API
[self callAppsFlyerV2API:purchaseTypeString
transactionId:transactionId
productId:productId
additionalParameters:additionalParameters
result:result];

} @catch (NSException *exception) {
NSLog(@"AppsFlyer: Error in validateAndLogInAppPurchaseV2: %@", exception.reason);
result([FlutterError errorWithCode:@"VALIDATION_ERROR"
message:[NSString stringWithFormat:@"Purchase validation failed: %@", exception.reason]
- (void)validateAndLogInAppPurchaseV2:(FlutterMethodCall*)call result:(FlutterResult)result {
NSDictionary* purchaseDetailsMap = call.arguments[@"purchaseDetails"];
NSDictionary* additionalParameters = call.arguments[@"additionalParameters"];

if (purchaseDetailsMap == nil) {
result([FlutterError errorWithCode:@"INVALID_ARGUMENTS"
message:@"Purchase details cannot be null"
details:nil]);
return;
}
}

- (void)callAppsFlyerV2API:(NSString*)purchaseTypeString
transactionId:(NSString*)transactionId
productId:(NSString*)productId
additionalParameters:(NSDictionary*)additionalParameters
result:(FlutterResult)result {

[[AppsFlyerLib shared] validateAndLogInAppPurchase:productId
price:nil // V2 doesn't use price
currency:nil // V2 doesn't use currency
transactionId:transactionId
additionalParameters:additionalParameters
success:^(NSDictionary *response) {
NSLog(@"AppsFlyer Debug: validateAndLogInAppPurchaseV2 Success!");
// V2 API returns response directly without wrapper
NSMutableDictionary *v2Response = [NSMutableDictionary dictionaryWithDictionary:response];
v2Response[@"purchase_type"] = purchaseTypeString;
result(v2Response);
NSString* purchaseTypeString = purchaseDetailsMap[@"purchaseType"];
NSString* transactionId = purchaseDetailsMap[@"purchaseToken"]; // purchaseToken maps to transactionId on iOS
NSString* productId = purchaseDetailsMap[@"productId"];

if (purchaseTypeString == nil || transactionId == nil || productId == nil) {
result([FlutterError errorWithCode:@"INVALID_ARGUMENTS"
message:@"Purchase details must contain purchaseType, purchaseToken, and productId"
details:nil]);
return;
}
failure:^(NSError *error, id errorResponse) {
NSLog(@"AppsFlyer Debug: validateAndLogInAppPurchaseV2 failed with Error: %@", error);

// Create error response for V2 format
NSMutableDictionary *errorData = [NSMutableDictionary dictionary];

// Map Dart enum to iOS AFSDKPurchaseType
AFSDKPurchaseType purchaseType = [purchaseTypeString isEqualToString:@"subscription"]
? AFSDKPurchaseTypeSubscription
: AFSDKPurchaseTypeOneTimePurchase;

AFSDKPurchaseDetails *purchaseDetails = [[AFSDKPurchaseDetails alloc] initWithProductId:productId
transactionId:transactionId
purchaseType:purchaseType];

// Handle NSNull for additionalParameters
NSDictionary* purchaseAdditionalDetails = [additionalParameters isEqual:[NSNull null]] ? nil : additionalParameters;

[[AppsFlyerLib shared] validateAndLogInAppPurchase:purchaseDetails
purchaseAdditionalDetails:purchaseAdditionalDetails
completion:^(NSDictionary * _Nullable response, NSError * _Nullable error) {
if (error) {
errorData[@"error_message"] = error.localizedDescription ?: @"Purchase validation failed";
errorData[@"error_code"] = @(error.code);
}
if (errorResponse && [errorResponse isKindOfClass:[NSDictionary class]]) {
[errorData addEntriesFromDictionary:(NSDictionary*)errorResponse];
NSLog(@"AppsFlyer Debug: validateAndLogInAppPurchaseV2 failed: %@", error.localizedDescription);
result([FlutterError errorWithCode:@"VALIDATION_ERROR"
message:error.localizedDescription ?: @"Purchase validation failed"
details:@{
@"error_code": @(error.code),
@"error_domain": error.domain ?: @"Unknown"
}]);
return;
}

result([FlutterError errorWithCode:@"VALIDATION_ERROR"
message:error.localizedDescription ?: @"Purchase validation failed"
details:errorData]);
NSLog(@"AppsFlyer Debug: validateAndLogInAppPurchaseV2 Success!");
result(response);
}];
}

Expand Down
6 changes: 3 additions & 3 deletions ios/appsflyer_sdk.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'appsflyer_sdk'
s.version = '6.17.7'
s.version = '6.17.8'
s.summary = 'AppsFlyer Integration for Flutter'
s.description = 'AppsFlyer is the market leader in mobile advertising attribution & analytics, helping marketers to pinpoint their targeting, optimize their ad spend and boost their ROI.'
s.homepage = 'https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk'
Expand All @@ -21,12 +21,12 @@ Pod::Spec.new do |s|
ss.source_files = 'Classes/**/*'
ss.public_header_files = 'Classes/**/*.h'
ss.dependency 'Flutter'
ss.ios.dependency 'AppsFlyerFramework','6.17.7'
ss.ios.dependency 'AppsFlyerFramework','6.17.8'
end

s.subspec 'PurchaseConnector' do |ss|
ss.dependency 'Flutter'
ss.ios.dependency 'PurchaseConnector', '6.17.7'
ss.ios.dependency 'PurchaseConnector', '6.17.8'
ss.source_files = 'PurchaseConnector/**/*'
ss.public_header_files = 'PurchaseConnector/**/*.h'

Expand Down
Loading